Skip to content

Commit eaf27e4

Browse files
authored
Merge pull request #1840 from hime/automated-cherry-pick-of-#1708-upstream-release-1.12
Automated cherry pick of #1708: Properly unwrap gce-compute error code.
2 parents 0fca5e7 + a9cbcdb commit eaf27e4

File tree

4 files changed

+177
-9
lines changed

4 files changed

+177
-9
lines changed

pkg/common/errors.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package common
15+
16+
import (
17+
"google.golang.org/grpc/codes"
18+
"google.golang.org/grpc/status"
19+
)
20+
21+
// TemporaryError wraps an error with a temporary error code.
22+
// It implements the error interface. Do not return TemporaryError
23+
// directly from CSI Spec API calls, as CSI Spec API calls MUST
24+
// return a standard gRPC status. If TemporaryErrors are returned from
25+
// helper functions within a CSI Spec API method, make sure the outer CSI
26+
// Spec API method returns a standard gRPC status. (e.g. LoggedError(tempErr) )
27+
type TemporaryError struct {
28+
err error
29+
code codes.Code
30+
}
31+
32+
// Unwrap extracts the original error.
33+
func (te *TemporaryError) Unwrap() error {
34+
return te.err
35+
}
36+
37+
// GRPCStatus extracts the underlying gRPC Status error.
38+
// This method is necessary to fulfill the grpcstatus interface
39+
// described in https://pkg.go.dev/google.golang.org/grpc/status#FromError.
40+
// FromError is used in CodeForError to get existing error codes from status errors.
41+
func (te *TemporaryError) GRPCStatus() *status.Status {
42+
if te.err == nil {
43+
return status.New(codes.OK, "")
44+
}
45+
return status.New(te.code, te.err.Error())
46+
}
47+
48+
func NewTemporaryError(code codes.Code, err error) *TemporaryError {
49+
return &TemporaryError{err: err, code: code}
50+
}
51+
52+
// Error returns a readable representation of the TemporaryError.
53+
func (te *TemporaryError) Error() string {
54+
return te.err.Error()
55+
}

pkg/common/utils.go

+52-9
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"
@@ -334,7 +335,6 @@ func CodeForError(sourceError error) codes.Code {
334335
if sourceError == nil {
335336
return codes.Internal
336337
}
337-
338338
if code, err := isUserMultiAttachError(sourceError); err == nil {
339339
return code
340340
}
@@ -347,12 +347,7 @@ func CodeForError(sourceError error) codes.Code {
347347
if code, err := isConnectionResetError(sourceError); err == nil {
348348
return code
349349
}
350-
351-
var apiErr *googleapi.Error
352-
if !errors.As(sourceError, &apiErr) {
353-
return codes.Internal
354-
}
355-
if code, ok := userErrorCodeMap[apiErr.Code]; ok {
350+
if code, err := isGoogleAPIError(sourceError); err == nil {
356351
return code
357352
}
358353

@@ -403,16 +398,64 @@ func isUserMultiAttachError(err error) (codes.Code, error) {
403398
return codes.Unknown, fmt.Errorf("Not a user multiattach error: %w", err)
404399
}
405400

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

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

pkg/common/utils_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"testing"
2727

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

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

test/e2e/tests/single_zone_e2e_test.go

+28
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
extremeDiskType = "pd-extreme"
@@ -268,6 +269,33 @@ var _ = Describe("GCE PD CSI Driver", func() {
268269
}
269270
})
270271

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

0 commit comments

Comments
 (0)