Skip to content

Commit a481f15

Browse files
committed
Properly unwrap gce-compute error code.
1 parent 7781c87 commit a481f15

File tree

3 files changed

+122
-9
lines changed

3 files changed

+122
-9
lines changed

pkg/common/utils.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"strings"
2626

2727
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta"
28+
"github.com/googleapis/gax-go/v2/apierror"
2829
"google.golang.org/api/googleapi"
2930
"google.golang.org/grpc/codes"
3031
"google.golang.org/grpc/status"
@@ -336,7 +337,6 @@ func CodeForError(sourceError error) codes.Code {
336337
if sourceError == nil {
337338
return codes.Internal
338339
}
339-
340340
if code, err := isUserMultiAttachError(sourceError); err == nil {
341341
return code
342342
}
@@ -349,12 +349,7 @@ func CodeForError(sourceError error) codes.Code {
349349
if code, err := isConnectionResetError(sourceError); err == nil {
350350
return code
351351
}
352-
353-
var apiErr *googleapi.Error
354-
if !errors.As(sourceError, &apiErr) {
355-
return codes.Internal
356-
}
357-
if code, ok := userErrorCodeMap[apiErr.Code]; ok {
352+
if code, err := isGoogleAPIError(sourceError); err == nil {
358353
return code
359354
}
360355

@@ -405,16 +400,64 @@ func isUserMultiAttachError(err error) (codes.Code, error) {
405400
return codes.Unknown, fmt.Errorf("Not a user multiattach error: %w", err)
406401
}
407402

403+
// existingErrorCode returns the existing gRPC Status error code for the given error, if one exists,
404+
// or an error if one doesn't exist. Since github.com/googleapis/gax-go/v2/apierror now wraps googleapi
405+
// errors (returned from GCE API calls), and sets their status error code to Unknown, we now have to
406+
// make sure we only return existing error codes from errors that are either TemporaryErrors, or errors
407+
// that do not wrap googleAPI errors. Otherwise, we will return Unknown for all GCE API calls that
408+
// return googleapi errors.
408409
func existingErrorCode(err error) (codes.Code, error) {
409410
if err == nil {
410411
return codes.Unknown, fmt.Errorf("null error")
411412
}
412-
if status, ok := status.FromError(err); ok {
413-
return status.Code(), nil
413+
var tmpError *TemporaryError
414+
// This explicitly checks our error is a temporary error before extracting its
415+
// status, as there can be other errors that can qualify as statusable
416+
// while not necessarily being temporary.
417+
if errors.As(err, &tmpError) {
418+
if status, ok := status.FromError(err); ok {
419+
return status.Code(), nil
420+
}
421+
}
422+
// We want to make sure we catch other error types that are statusable.
423+
// (eg. grpc-go/internal/status/status.go Error struct that wraps a status)
424+
var googleErr *googleapi.Error
425+
if !errors.As(err, &googleErr) {
426+
if status, ok := status.FromError(err); ok {
427+
return status.Code(), nil
428+
}
414429
}
430+
415431
return codes.Unknown, fmt.Errorf("no existing error code for %w", err)
416432
}
417433

434+
// isGoogleAPIError returns the gRPC status code for the given googleapi error by mapping
435+
// the googleapi error's HTTP code to the corresponding gRPC error code. If the error is
436+
// wrapped in an APIError (github.com/googleapis/gax-go/v2/apierror), it maps the wrapped
437+
// googleAPI error's HTTP code to the corresponding gRPC error code. Returns an error if
438+
// the given error is not a googleapi error.
439+
func isGoogleAPIError(err error) (codes.Code, error) {
440+
var googleErr *googleapi.Error
441+
if !errors.As(err, &googleErr) {
442+
return codes.Unknown, fmt.Errorf("error %w is not a googleapi.Error", err)
443+
}
444+
var sourceCode int
445+
var apiErr *apierror.APIError
446+
if errors.As(err, &apiErr) {
447+
// When googleapi.Err is used as a wrapper, we return the error code of the wrapped contents.
448+
sourceCode = apiErr.HTTPCode()
449+
} else {
450+
// Rely on error code in googleapi.Err when it is our primary error.
451+
sourceCode = googleErr.Code
452+
}
453+
// Map API error code to user error code.
454+
if code, ok := userErrorCodeMap[sourceCode]; ok {
455+
return code, nil
456+
}
457+
458+
return codes.Unknown, fmt.Errorf("googleapi.Error %w does not map to any known errors", err)
459+
}
460+
418461
func LoggedError(msg string, err error) error {
419462
klog.Errorf(msg+"%v", err.Error())
420463
return status.Errorf(CodeForError(err), msg+"%v", err.Error())

pkg/common/utils_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta"
2929
"github.com/google/go-cmp/cmp"
30+
"github.com/googleapis/gax-go/v2/apierror"
3031
"google.golang.org/api/googleapi"
3132
"google.golang.org/grpc/codes"
3233
"google.golang.org/grpc/status"
@@ -977,6 +978,17 @@ func TestParseMachineType(t *testing.T) {
977978
}
978979

979980
func TestCodeForError(t *testing.T) {
981+
getGoogleAPIWrappedError := func(err error) *googleapi.Error {
982+
apierr, _ := apierror.ParseError(err, false)
983+
wrappedError := &googleapi.Error{}
984+
wrappedError.Wrap(apierr)
985+
986+
return wrappedError
987+
}
988+
getAPIError := func(err error) *apierror.APIError {
989+
apierror, _ := apierror.ParseError(err, true)
990+
return apierror
991+
}
980992
testCases := []struct {
981993
name string
982994
inputErr error
@@ -992,6 +1004,36 @@ func TestCodeForError(t *testing.T) {
9921004
inputErr: &googleapi.Error{Code: http.StatusBadRequest, Message: "User error with bad request"},
9931005
expCode: codes.InvalidArgument,
9941006
},
1007+
{
1008+
name: "googleapi.Error that wraps apierror.APIError of http kind",
1009+
inputErr: getGoogleAPIWrappedError(&googleapi.Error{
1010+
Code: 404,
1011+
Message: "data requested not found error",
1012+
}),
1013+
expCode: codes.NotFound,
1014+
},
1015+
{
1016+
name: "googleapi.Error that wraps apierror.APIError of status kind",
1017+
inputErr: getGoogleAPIWrappedError(status.New(
1018+
codes.Internal, "Internal status error",
1019+
).Err()),
1020+
expCode: codes.Internal,
1021+
},
1022+
{
1023+
name: "apierror.APIError of http kind",
1024+
inputErr: getAPIError(&googleapi.Error{
1025+
Code: 404,
1026+
Message: "data requested not found error",
1027+
}),
1028+
expCode: codes.NotFound,
1029+
},
1030+
{
1031+
name: "apierror.APIError of status kind",
1032+
inputErr: getAPIError(status.New(
1033+
codes.Canceled, "Internal status error",
1034+
).Err()),
1035+
expCode: codes.Canceled,
1036+
},
9951037
{
9961038
name: "googleapi.Error but not a user error",
9971039
inputErr: &googleapi.Error{Code: http.StatusInternalServerError, Message: "Internal error"},

test/e2e/tests/single_zone_e2e_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const (
5050
defaultRepdSizeGb int64 = 200
5151
defaultMwSizeGb int64 = 200
5252
defaultVolumeLimit int64 = 127
53+
invalidSizeGb int64 = 66000
5354
readyState = "READY"
5455
standardDiskType = "pd-standard"
5556
ssdDiskType = "pd-ssd"
@@ -269,6 +270,33 @@ var _ = Describe("GCE PD CSI Driver", func() {
269270
}
270271
})
271272

273+
It("Should return InvalidArgument when disk size exceeds limit", func() {
274+
// If this returns a different error code (like Unknown), the error wrapping logic in #1708 has regressed.
275+
Expect(testContexts).ToNot(BeEmpty())
276+
testContext := getRandomTestContext()
277+
278+
zones := []string{"us-central1-c", "us-central1-b", "us-central1-a"}
279+
280+
for _, zone := range zones {
281+
volName := testNamePrefix + string(uuid.NewUUID())
282+
topReq := &csi.TopologyRequirement{
283+
Requisite: []*csi.Topology{
284+
{
285+
Segments: map[string]string{common.TopologyKeyZone: zone},
286+
},
287+
},
288+
}
289+
volume, err := testContext.Client.CreateVolume(volName, nil, invalidSizeGb, topReq, nil)
290+
Expect(err).ToNot(BeNil(), "Failed to fetch error from create volume.")
291+
Expect(err.Error()).To(ContainSubstring("InvalidArgument"), "Failed to verify error code matches InvalidArgument.")
292+
defer func() {
293+
if volume != nil {
294+
testContext.Client.DeleteVolume(volume.VolumeId)
295+
}
296+
}()
297+
}
298+
})
299+
272300
DescribeTable("Should complete entire disk lifecycle with underspecified volume ID",
273301
func(diskType string) {
274302
testContext := getRandomTestContext()

0 commit comments

Comments
 (0)