From 95dee2ac079a18755e8656a4b701003f6b737775 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Fri, 12 May 2023 08:17:30 -0700 Subject: [PATCH 1/4] Retry policy will always clone the *http.Request (#20843) * Retry policy will always clone the *http.Request This ensures that the entirety of the request is restored on retries. * simplify test --- sdk/azcore/CHANGELOG.md | 1 + sdk/azcore/runtime/policy_retry.go | 3 ++- sdk/azcore/runtime/policy_retry_test.go | 33 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md index df18a5205d4e..bb0c80a3273a 100644 --- a/sdk/azcore/CHANGELOG.md +++ b/sdk/azcore/CHANGELOG.md @@ -7,6 +7,7 @@ ### Breaking Changes ### Bugs Fixed +* Retry policy always clones the underlying `*http.Request` before invoking the next policy. ### Other Changes diff --git a/sdk/azcore/runtime/policy_retry.go b/sdk/azcore/runtime/policy_retry.go index 5f52ba75b459..e0c5929f3b70 100644 --- a/sdk/azcore/runtime/policy_retry.go +++ b/sdk/azcore/runtime/policy_retry.go @@ -125,7 +125,8 @@ func (p *retryPolicy) Do(req *policy.Request) (resp *http.Response, err error) { } if options.TryTimeout == 0 { - resp, err = req.Next() + clone := req.Clone(req.Raw().Context()) + resp, err = clone.Next() } else { // Set the per-try time for this particular retry operation and then Do the operation. tryCtx, tryCancel := context.WithTimeout(req.Raw().Context(), options.TryTimeout) diff --git a/sdk/azcore/runtime/policy_retry_test.go b/sdk/azcore/runtime/policy_retry_test.go index 61ce081b4204..228d0931a025 100644 --- a/sdk/azcore/runtime/policy_retry_test.go +++ b/sdk/azcore/runtime/policy_retry_test.go @@ -810,6 +810,39 @@ func TestRetryableRequestBodyWithCloser(t *testing.T) { require.True(t, tr.closeCalled) } +func TestRetryPolicySuccessWithRetryPreserveHeaders(t *testing.T) { + srv, close := mock.NewServer() + defer close() + srv.AppendResponse(mock.WithStatusCode(http.StatusRequestTimeout)) + srv.AppendResponse() + pl := exported.NewPipeline(srv, NewRetryPolicy(testRetryOptions()), exported.PolicyFunc(challengeLikePolicy)) + req, err := NewRequest(context.Background(), http.MethodGet, srv.URL()) + require.NoError(t, err) + body := newRewindTrackingBody("stuff") + require.NoError(t, req.SetBody(body, "text/plain")) + resp, err := pl.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + require.EqualValues(t, 2, srv.Requests()) + require.EqualValues(t, 1, body.rcount) + require.True(t, body.closed) +} + +func challengeLikePolicy(req *policy.Request) (*http.Response, error) { + if req.Body() == nil { + return nil, errors.New("request body wasn't restored") + } + if req.Raw().Header.Get("content-type") != "text/plain" { + return nil, errors.New("content-type header wasn't restored") + } + + // remove the body and header. the retry policy should restore them + if err := req.SetBody(nil, ""); err != nil { + return nil, err + } + return req.Next() +} + func newRewindTrackingBody(s string) *rewindTrackingBody { // there are two rewinds that happen before rewinding for a retry // 1. to get the body's size in SetBody() From f96c88448a6ee3b913be35e7ef0bb99b0e8aa859 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Fri, 12 May 2023 10:14:21 -0700 Subject: [PATCH 2/4] Handle more error codes when an RP isn't registered (#20848) There's more than one error code returned from unregistered RPs. --- sdk/azcore/CHANGELOG.md | 1 + sdk/azcore/arm/runtime/pipeline_test.go | 2 +- sdk/azcore/arm/runtime/policy_register_rp.go | 17 +++++++++-- .../arm/runtime/policy_register_rp_test.go | 29 ++++++++++++++----- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md index bb0c80a3273a..0a4ea3c230be 100644 --- a/sdk/azcore/CHANGELOG.md +++ b/sdk/azcore/CHANGELOG.md @@ -8,6 +8,7 @@ ### Bugs Fixed * Retry policy always clones the underlying `*http.Request` before invoking the next policy. +* Added `MissingRegistrationForResourceProvider` to the list of error codes for unregistered resource providers. ### Other Changes diff --git a/sdk/azcore/arm/runtime/pipeline_test.go b/sdk/azcore/arm/runtime/pipeline_test.go index 1fc6fef99aeb..d48a03f14fe8 100644 --- a/sdk/azcore/arm/runtime/pipeline_test.go +++ b/sdk/azcore/arm/runtime/pipeline_test.go @@ -104,7 +104,7 @@ func TestDisableAutoRPRegistration(t *testing.T) { srv, close := mock.NewServer() defer close() // initial response that RP is unregistered - srv.SetResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.SetResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp1))) opts := &armpolicy.ClientOptions{DisableRPRegistration: true, ClientOptions: policy.ClientOptions{Transport: srv}} req, err := azruntime.NewRequest(context.Background(), http.MethodGet, srv.URL()) if err != nil { diff --git a/sdk/azcore/arm/runtime/policy_register_rp.go b/sdk/azcore/arm/runtime/policy_register_rp.go index 49e6608070f2..f6930b4286c3 100644 --- a/sdk/azcore/arm/runtime/policy_register_rp.go +++ b/sdk/azcore/arm/runtime/policy_register_rp.go @@ -80,7 +80,6 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error) // policy is disabled return req.Next() } - const unregisteredRPCode = "MissingSubscriptionRegistration" const registeredState = "Registered" var rp string var resp *http.Response @@ -101,7 +100,7 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error) // to the caller so its error unmarshalling will kick in return resp, err } - if !strings.EqualFold(reqErr.ServiceError.Code, unregisteredRPCode) { + if !isUnregisteredRPCode(reqErr.ServiceError.Code) { // not a 409 due to unregistered RP. just return the response // to the caller so its error unmarshalling will kick in return resp, err @@ -173,6 +172,20 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error) return resp, fmt.Errorf("exceeded attempts to register %s", rp) } +var unregisteredRPCodes = []string{ + "MissingSubscriptionRegistration", + "MissingRegistrationForResourceProvider", +} + +func isUnregisteredRPCode(errorCode string) bool { + for _, code := range unregisteredRPCodes { + if strings.EqualFold(errorCode, code) { + return true + } + } + return false +} + func getSubscription(path string) (string, error) { parts := strings.Split(path, "/") for i, v := range parts { diff --git a/sdk/azcore/arm/runtime/policy_register_rp_test.go b/sdk/azcore/arm/runtime/policy_register_rp_test.go index 567e38fd4e87..b38d90250e6d 100644 --- a/sdk/azcore/arm/runtime/policy_register_rp_test.go +++ b/sdk/azcore/arm/runtime/policy_register_rp_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/require" ) -const rpUnregisteredResp = `{ +const rpUnregisteredResp1 = `{ "error":{ "code":"MissingSubscriptionRegistration", "message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.Storage'. See https://aka.ms/rps-not-found for how to register subscriptions.", @@ -37,6 +37,19 @@ const rpUnregisteredResp = `{ } }` +const rpUnregisteredResp2 = `{ + "error":{ + "code":"MissingRegistrationForResourceProvider", + "message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.Storage'. See https://aka.ms/rps-not-found for how to register subscriptions.", + "details":[{ + "code":"MissingRegistrationForResourceProvider", + "target":"Microsoft.Storage", + "message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.Storage'. See https://aka.ms/rps-not-found for how to register subscriptions." + } + ] + } +}` + // some content was omitted here as it's not relevant const rpRegisteringResp = `{ "id": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Storage", @@ -89,7 +102,7 @@ func TestRPRegistrationPolicySuccess(t *testing.T) { srv, close := mock.NewServer() defer close() // initial response that RP is unregistered - srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp1))) // polling responses to Register() and Get(), in progress srv.RepeatResponse(5, mock.WithStatusCode(http.StatusOK), mock.WithBody([]byte(rpRegisteringResp))) // polling response, successful registration @@ -180,7 +193,7 @@ func TestRPRegistrationPolicyTimesOut(t *testing.T) { srv, close := mock.NewServer() defer close() // initial response that RP is unregistered - srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp1))) // polling responses to Register() and Get(), in progress but slow // tests registration takes too long, times out srv.RepeatResponse(10, mock.WithStatusCode(http.StatusOK), mock.WithBody([]byte(rpRegisteringResp)), mock.WithSlowResponse(400*time.Millisecond)) @@ -212,7 +225,7 @@ func TestRPRegistrationPolicyExceedsAttempts(t *testing.T) { // add a cycle of unregistered->registered so that we keep retrying and hit the cap for i := 0; i < 4; i++ { // initial response that RP is unregistered - srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp1))) // polling responses to Register() and Get(), in progress srv.RepeatResponse(2, mock.WithStatusCode(http.StatusOK), mock.WithBody([]byte(rpRegisteringResp))) // polling response, successful registration @@ -246,7 +259,7 @@ func TestRPRegistrationPolicyCanCancel(t *testing.T) { srv, close := mock.NewServer() defer close() // initial response that RP is unregistered - srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp2))) // polling responses to Register() and Get(), in progress but slow so we have time to cancel srv.RepeatResponse(10, mock.WithStatusCode(http.StatusOK), mock.WithBody([]byte(rpRegisteringResp)), mock.WithSlowResponse(300*time.Millisecond)) // log only RP registration @@ -287,7 +300,7 @@ func TestRPRegistrationPolicyDisabled(t *testing.T) { srv, close := mock.NewServer() defer close() // initial response that RP is unregistered - srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp2))) ops := testRPRegistrationOptions(srv) ops.MaxAttempts = -1 client := newFakeClient(t, srv, ops) @@ -305,7 +318,7 @@ func TestRPRegistrationPolicyDisabled(t *testing.T) { require.Error(t, err) var respErr *exported.ResponseError require.ErrorAs(t, err, &respErr) - require.EqualValues(t, "MissingSubscriptionRegistration", respErr.ErrorCode) + require.EqualValues(t, "MissingRegistrationForResourceProvider", respErr.ErrorCode) require.Zero(t, resp) // shouldn't be any log entries require.Zero(t, logEntries) @@ -315,7 +328,7 @@ func TestRPRegistrationPolicyAudience(t *testing.T) { srv, close := mock.NewServer() defer close() // initial response that RP is unregistered - srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp))) + srv.AppendResponse(mock.WithStatusCode(http.StatusConflict), mock.WithBody([]byte(rpUnregisteredResp2))) // polling responses to Register() and Get(), in progress srv.AppendResponse(mock.WithStatusCode(http.StatusOK), mock.WithBody([]byte(rpRegisteringResp))) // polling response, successful registration From aa8c2bcca66d817c54f9a547f1d9ebfb7145f584 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Tue, 16 May 2023 12:56:10 -0700 Subject: [PATCH 3/4] Add another non-standard error code for RP registration (#20860) --- sdk/azcore/CHANGELOG.md | 2 +- sdk/azcore/arm/runtime/policy_register_rp.go | 1 + sdk/azcore/arm/runtime/policy_register_rp_test.go | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md index 0a4ea3c230be..39139aa758d4 100644 --- a/sdk/azcore/CHANGELOG.md +++ b/sdk/azcore/CHANGELOG.md @@ -8,7 +8,7 @@ ### Bugs Fixed * Retry policy always clones the underlying `*http.Request` before invoking the next policy. -* Added `MissingRegistrationForResourceProvider` to the list of error codes for unregistered resource providers. +* Added some non-standard error codes to the list of error codes for unregistered resource providers. ### Other Changes diff --git a/sdk/azcore/arm/runtime/policy_register_rp.go b/sdk/azcore/arm/runtime/policy_register_rp.go index f6930b4286c3..c3f5eeafe020 100644 --- a/sdk/azcore/arm/runtime/policy_register_rp.go +++ b/sdk/azcore/arm/runtime/policy_register_rp.go @@ -175,6 +175,7 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error) var unregisteredRPCodes = []string{ "MissingSubscriptionRegistration", "MissingRegistrationForResourceProvider", + "Subscription Not Registered", } func isUnregisteredRPCode(errorCode string) bool { diff --git a/sdk/azcore/arm/runtime/policy_register_rp_test.go b/sdk/azcore/arm/runtime/policy_register_rp_test.go index b38d90250e6d..0edb866bf34e 100644 --- a/sdk/azcore/arm/runtime/policy_register_rp_test.go +++ b/sdk/azcore/arm/runtime/policy_register_rp_test.go @@ -412,6 +412,11 @@ func TestRPRegistrationPolicyEnvironmentsInSubExceeded(t *testing.T) { require.EqualValues(t, 0, logEntries) } +func TestIsUnregisteredRPCode(t *testing.T) { + require.True(t, isUnregisteredRPCode("Subscription Not Registered")) + require.False(t, isUnregisteredRPCode("Your subscription isn't registered")) +} + type fakeClient struct { ep string pl runtime.Pipeline From 1b18d897275c2466a9af86a47b1dfcfb7f5a459f Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Mon, 5 Jun 2023 10:14:20 -0700 Subject: [PATCH 4/4] Prep azcore v1.6.1 for release Cherry-picked the following commits. - a3b2c13cc1ead2102feb0bd20c4e304a08812fe1 - 889be5860054a17042773c62f658f655a77fec97 - 6879d7e3f9977286c4b28a52e921ca3343d67ade --- sdk/azcore/CHANGELOG.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md index 39139aa758d4..91d9a743ff83 100644 --- a/sdk/azcore/CHANGELOG.md +++ b/sdk/azcore/CHANGELOG.md @@ -1,17 +1,11 @@ # Release History -## 1.6.1 (Unreleased) - -### Features Added - -### Breaking Changes +## 1.6.1 (2023-06-06) ### Bugs Fixed * Retry policy always clones the underlying `*http.Request` before invoking the next policy. * Added some non-standard error codes to the list of error codes for unregistered resource providers. -### Other Changes - ## 1.6.0 (2023-05-04) ### Features Added