@@ -22,6 +22,7 @@ import (
2222 "fmt"
2323 "net/http"
2424 "regexp"
25+ "slices"
2526 "strings"
2627 "time"
2728
@@ -73,6 +74,10 @@ const (
7374 // Full or partial URL of the machine type resource, in the format:
7475 // zones/zone/machineTypes/machine-type
7576 machineTypePattern = "zones/[^/]+/machineTypes/([^/]+)$"
77+
78+ // Full or partial URL of the zone resource, in the format:
79+ // projects/{project}/zones/{zone}
80+ zoneURIPattern = "projects/[^/]+/zones/([^/]+)$"
7681)
7782
7883var (
8590
8691 storagePoolFieldsRegex = regexp .MustCompile (`^projects/([^/]+)/zones/([^/]+)/storagePools/([^/]+)$` )
8792
93+ zoneURIRegex = regexp .MustCompile (zoneURIPattern )
94+
8895 // userErrorCodeMap tells how API error types are translated to error codes.
8996 userErrorCodeMap = map [int ]codes.Code {
9097 http .StatusForbidden : codes .PermissionDenied ,
97104 regexParent = regexp .MustCompile (`(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)` )
98105 regexKey = regexp .MustCompile (`^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$` )
99106 regexValue = regexp .MustCompile (`^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\[\]{}\-\s]{0,61}[a-zA-Z0-9])?$` )
107+
108+ csiRetryableErrorCodes = []codes.Code {codes .Canceled , codes .DeadlineExceeded , codes .Unavailable , codes .Aborted , codes .ResourceExhausted }
100109)
101110
102111func BytesToGbRoundDown (bytes int64 ) int64 {
@@ -545,9 +554,37 @@ func isGoogleAPIError(err error) (codes.Code, error) {
545554 return codes .Unknown , fmt .Errorf ("googleapi.Error %w does not map to any known errors" , err )
546555}
547556
548- func LoggedError (msg string , err error ) error {
557+ func loggedErrorForCode (msg string , code codes. Code , err error ) error {
549558 klog .Errorf (msg + "%v" , err .Error ())
550- return status .Errorf (CodeForError (err ), msg + "%v" , err .Error ())
559+ return status .Errorf (code , msg + "%v" , err .Error ())
560+ }
561+
562+ func LoggedError (msg string , err error ) error {
563+ return loggedErrorForCode (msg , CodeForError (err ), err )
564+ }
565+
566+ // NewCombinedError tries to return an appropriate wrapped error that captures
567+ // useful information as an error code
568+ // If there are multiple errors, it extracts the first "retryable" error
569+ // as interpreted by the CSI sidecar.
570+ func NewCombinedError (msg string , errs []error ) error {
571+ // If there is only one error, return it as the single error code
572+ if len (errs ) == 1 {
573+ LoggedError (msg , errs [0 ])
574+ }
575+
576+ for _ , err := range errs {
577+ code := CodeForError (err )
578+ if slices .Contains (csiRetryableErrorCodes , code ) {
579+ // Return this as a TemporaryError to lock-in the retryable code
580+ // This will invoke the "existing" error code check in CodeForError
581+ return NewTemporaryError (code , fmt .Errorf ("%s: %w" , msg , err ))
582+ }
583+ }
584+
585+ // None of these error codes were retryable. Just return a combined error
586+ // The first matching error (based on our CodeForError) logic will be returned.
587+ return LoggedError (msg , errors .Join (errs ... ))
551588}
552589
553590func isValidDiskEncryptionKmsKey (DiskEncryptionKmsKey string ) bool {
@@ -556,6 +593,14 @@ func isValidDiskEncryptionKmsKey(DiskEncryptionKmsKey string) bool {
556593 return kmsKeyPattern .MatchString (DiskEncryptionKmsKey )
557594}
558595
596+ func ParseZoneFromURI (zoneURI string ) (string , error ) {
597+ zoneMatch := zoneURIRegex .FindStringSubmatch (zoneURI )
598+ if zoneMatch == nil {
599+ return "" , fmt .Errorf ("failed to parse zone URI. Expected projects/{project}/zones/{zone}. Got: %s" , zoneURI )
600+ }
601+ return zoneMatch [1 ], nil
602+ }
603+
559604// ParseStoragePools returns an error if none of the given storagePools
560605// (delimited by a comma) are in the format
561606// projects/project/zones/zone/storagePools/storagePool.
0 commit comments