Skip to content

Commit 3b7a1fe

Browse files
emilymyemodular-magician
authored andcommitted
Add retries/backoff when (only) reading IAM policies
Signed-off-by: Modular Magician <[email protected]>
1 parent 9ac574e commit 3b7a1fe

6 files changed

+106
-5
lines changed

google/iam.go

+39-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"google.golang.org/api/cloudresourcemanager/v1"
1111
)
1212

13+
const maxBackoffSeconds = 30
14+
1315
// The ResourceIamUpdater interface is implemented for each GCP resource supporting IAM policy.
1416
//
1517
// Implementations should keep track of the resource identifier.
@@ -44,6 +46,41 @@ type iamPolicyModifyFunc func(p *cloudresourcemanager.Policy) error
4446

4547
type resourceIdParserFunc func(d *schema.ResourceData, config *Config) error
4648

49+
// Wrapper around updater.GetResourceIamPolicy() that handles Fibonacci backoff
50+
// for reading policies from IAM
51+
func iamPolicyReadWithRetry(updater ResourceIamUpdater) (*cloudresourcemanager.Policy, error) {
52+
mutexKey := updater.GetMutexKey()
53+
mutexKV.Lock(mutexKey)
54+
defer mutexKV.Unlock(mutexKey)
55+
56+
backoff := time.Second
57+
prevBackoff := time.Duration(0)
58+
59+
for {
60+
log.Printf("[DEBUG] Retrieving policy for %s\n", updater.DescribeResource())
61+
p, err := updater.GetResourceIamPolicy()
62+
if err == nil {
63+
log.Printf("[DEBUG] Retrieved policy for %s: %+v\n", updater.DescribeResource(), p)
64+
return p, nil
65+
}
66+
67+
if !isGoogleApiErrorWithCode(err, 429) {
68+
return nil, err
69+
}
70+
71+
if backoff > time.Second*maxBackoffSeconds {
72+
return nil, fmt.Errorf("Error applying IAM policy to %s: Waited too long for read.\n", updater.DescribeResource())
73+
}
74+
75+
log.Printf("[DEBUG] 429 while attempting to read policy for %s, waiting %v before attempting again", updater.DescribeResource(), backoff)
76+
time.Sleep(backoff)
77+
// Fibonacci increase backoff
78+
newBackoff := backoff + prevBackoff
79+
prevBackoff = backoff
80+
backoff = newBackoff
81+
}
82+
}
83+
4784
func iamPolicyReadModifyWrite(updater ResourceIamUpdater, modify iamPolicyModifyFunc) error {
4885
mutexKey := updater.GetMutexKey()
4986
mutexKV.Lock(mutexKey)
@@ -54,6 +91,7 @@ func iamPolicyReadModifyWrite(updater ResourceIamUpdater, modify iamPolicyModify
5491
log.Printf("[DEBUG]: Retrieving policy for %s\n", updater.DescribeResource())
5592
p, err := updater.GetResourceIamPolicy()
5693
if isGoogleApiErrorWithCode(err, 429) {
94+
log.Printf("[DEBUG] 429 while attempting to read policy for %s, waiting %v before attempting again", updater.DescribeResource(), backoff)
5795
time.Sleep(backoff)
5896
continue
5997
} else if err != nil {
@@ -71,7 +109,7 @@ func iamPolicyReadModifyWrite(updater ResourceIamUpdater, modify iamPolicyModify
71109
if err == nil {
72110
fetchBackoff := 1 * time.Second
73111
for successfulFetches := 0; successfulFetches < 3; {
74-
if fetchBackoff > 30*time.Second {
112+
if fetchBackoff > maxBackoffSeconds*time.Second {
75113
return fmt.Errorf("Error applying IAM policy to %s: Waited too long for propagation.\n", updater.DescribeResource())
76114
}
77115
time.Sleep(fetchBackoff)

google/iam_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package google
2+
3+
import (
4+
"google.golang.org/api/cloudresourcemanager/v1"
5+
"google.golang.org/api/googleapi"
6+
"testing"
7+
)
8+
9+
// Mock ResourceIamUpdater
10+
type readTestUpdater struct {
11+
errorCode int
12+
returnAfterAttempts int
13+
numAttempts int
14+
}
15+
16+
// Not being tested
17+
func (u *readTestUpdater) SetResourceIamPolicy(p *cloudresourcemanager.Policy) error { return nil }
18+
func (u *readTestUpdater) GetMutexKey() string { return "a-resource-key" }
19+
func (u *readTestUpdater) GetResourceId() string { return "id" }
20+
func (u *readTestUpdater) DescribeResource() string { return "mock updater" }
21+
22+
// Fetch the existing IAM policy attached to a resource.
23+
func (u *readTestUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
24+
u.numAttempts++
25+
if u.numAttempts < u.returnAfterAttempts {
26+
// Indicate retry
27+
return nil, &googleapi.Error{Code: 429}
28+
}
29+
30+
// Was testing successes or was testing retryable
31+
if u.errorCode == 200 {
32+
return &cloudresourcemanager.Policy{}, nil
33+
}
34+
return nil, &googleapi.Error{Code: u.errorCode}
35+
}
36+
37+
func TestIamPolicyReadWithRetry_returnImmediately(t *testing.T) {
38+
mockUpdater := &readTestUpdater{
39+
returnAfterAttempts: 1,
40+
errorCode: 200,
41+
}
42+
p, err := iamPolicyReadWithRetry(mockUpdater)
43+
if err != nil || p == nil {
44+
t.Errorf("expected valid policy and no error, got nil policy and error %v", err)
45+
}
46+
if mockUpdater.numAttempts != 1 {
47+
t.Errorf("expected GetResourceIamPolicy to have been called once")
48+
}
49+
}
50+
51+
func TestIamPolicyReadWithRetry_retry(t *testing.T) {
52+
mockUpdater := &readTestUpdater{
53+
returnAfterAttempts: 3,
54+
errorCode: 404,
55+
}
56+
p, err := iamPolicyReadWithRetry(mockUpdater)
57+
if err == nil || !isGoogleApiErrorWithCode(err, 404) {
58+
t.Errorf("expected googleapi error 404, got policy %v, err %v", p, err)
59+
}
60+
if mockUpdater.numAttempts != mockUpdater.returnAfterAttempts {
61+
t.Errorf("expected GetResourceIamPolicy to have been called %d times, was called %d", mockUpdater.numAttempts, mockUpdater.returnAfterAttempts)
62+
}
63+
}

google/resource_iam_audit_config.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func resourceIamAuditConfigRead(newUpdaterFunc newResourceIamUpdaterFunc) schema
8686
}
8787

8888
eAuditConfig := getResourceIamAuditConfig(d)
89-
p, err := updater.GetResourceIamPolicy()
89+
p, err := iamPolicyReadWithRetry(updater)
9090
if err != nil {
9191
if isGoogleApiErrorWithCode(err, 404) {
9292
log.Printf("[DEBUG]: AuditConfig for service %q not found for non-existent resource %s, removing from state file.", eAuditConfig.Service, updater.DescribeResource())

google/resource_iam_binding.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func resourceIamBindingRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Rea
7777
}
7878

7979
eBinding := getResourceIamBinding(d)
80-
p, err := updater.GetResourceIamPolicy()
80+
p, err := iamPolicyReadWithRetry(updater)
8181
if err != nil {
8282
if isGoogleApiErrorWithCode(err, 404) {
8383
log.Printf("[DEBUG]: Binding for role %q not found for non-existent resource %s, removing from state file.", updater.DescribeResource(), eBinding.Role)

google/resource_iam_member.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func resourceIamMemberRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Read
112112
}
113113

114114
eMember := getResourceIamMember(d)
115-
p, err := updater.GetResourceIamPolicy()
115+
p, err := iamPolicyReadWithRetry(updater)
116116
if err != nil {
117117
if isGoogleApiErrorWithCode(err, 404) {
118118
log.Printf("[DEBUG]: Binding of member %q with role %q does not exist for non-existent resource %s, removing from state.", eMember.Members[0], eMember.Role, updater.DescribeResource())

google/resource_iam_policy.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func ResourceIamPolicyRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Read
8181
return err
8282
}
8383

84-
policy, err := updater.GetResourceIamPolicy()
84+
policy, err := iamPolicyReadWithRetry(updater)
8585
if err != nil {
8686
if isGoogleApiErrorWithCode(err, 404) {
8787
log.Printf("[DEBUG]: Policy does not exist for non-existent resource %q", updater.GetResourceId())

0 commit comments

Comments
 (0)