forked from Azure/go-autorest
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto register resource providers (Azure#176)
* Progress * Clean * Get unwanted changes back * MOre cleaning * And more * Delete unused parameters * Exported Status codes * Added test * Fix CI * Added polling, better way for getting provider * Fixes retriable request issue * More complete test * Added configurable polling delay and duration * Rework with an azure client * Make this actually testable * Fix CI * feedback * Removed sender parameter * Rework without azure specific client * Adding sender comment * Removed reset references * Feedback * Exit early * Correct loop * remove unused lines * feedback * More explanation
- Loading branch information
Showing
5 changed files
with
279 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
package azure | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
|
||
"github.com/Azure/go-autorest/autorest" | ||
) | ||
|
||
// DoRetryWithRegistration tries to register the resource provider in case it is unregistered. | ||
// It also handles request retries | ||
func DoRetryWithRegistration(client autorest.Client) autorest.SendDecorator { | ||
return func(s autorest.Sender) autorest.Sender { | ||
return autorest.SenderFunc(func(r *http.Request) (resp *http.Response, err error) { | ||
rr := autorest.NewRetriableRequest(r) | ||
for currentAttempt := 0; currentAttempt < client.RetryAttempts; currentAttempt++ { | ||
err = rr.Prepare() | ||
if err != nil { | ||
return resp, err | ||
} | ||
|
||
resp, err = autorest.SendWithSender(s, rr.Request(), | ||
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...), | ||
) | ||
if err != nil { | ||
return resp, err | ||
} | ||
|
||
if resp.StatusCode != http.StatusConflict { | ||
return resp, err | ||
} | ||
var re RequestError | ||
err = autorest.Respond( | ||
resp, | ||
autorest.ByUnmarshallingJSON(&re), | ||
) | ||
if err != nil { | ||
return resp, err | ||
} | ||
|
||
if re.ServiceError != nil && re.ServiceError.Code == "MissingSubscriptionRegistration" { | ||
err = register(client, r, re) | ||
if err != nil { | ||
return resp, fmt.Errorf("failed auto registering Resource Provider: %s", err) | ||
} | ||
} | ||
} | ||
return resp, errors.New("failed request and resource provider registration") | ||
}) | ||
} | ||
} | ||
|
||
func getProvider(re RequestError) (string, error) { | ||
if re.ServiceError != nil { | ||
if re.ServiceError.Details != nil && len(*re.ServiceError.Details) > 0 { | ||
detail := (*re.ServiceError.Details)[0].(map[string]interface{}) | ||
return detail["target"].(string), nil | ||
} | ||
} | ||
return "", errors.New("provider was not found in the response") | ||
} | ||
|
||
func register(client autorest.Client, originalReq *http.Request, re RequestError) error { | ||
subID := getSubscription(originalReq.URL.Path) | ||
if subID == "" { | ||
return errors.New("missing parameter subscriptionID to register resource provider") | ||
} | ||
providerName, err := getProvider(re) | ||
if err != nil { | ||
return fmt.Errorf("missing parameter provider to register resource provider: %s", err) | ||
} | ||
newURL := url.URL{ | ||
Scheme: originalReq.URL.Scheme, | ||
Host: originalReq.URL.Host, | ||
} | ||
|
||
// taken from the resources SDK | ||
// with almost identical code, this sections are easier to mantain | ||
// It is also not a good idea to import the SDK here | ||
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L252 | ||
pathParameters := map[string]interface{}{ | ||
"resourceProviderNamespace": autorest.Encode("path", providerName), | ||
"subscriptionId": autorest.Encode("path", subID), | ||
} | ||
|
||
const APIVersion = "2016-09-01" | ||
queryParameters := map[string]interface{}{ | ||
"api-version": APIVersion, | ||
} | ||
|
||
preparer := autorest.CreatePreparer( | ||
autorest.AsPost(), | ||
autorest.WithBaseURL(newURL.String()), | ||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}/register", pathParameters), | ||
autorest.WithQueryParameters(queryParameters), | ||
) | ||
|
||
req, err := preparer.Prepare(&http.Request{}) | ||
if err != nil { | ||
return err | ||
} | ||
req.Cancel = originalReq.Cancel | ||
|
||
resp, err := autorest.SendWithSender(client, req, | ||
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...), | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
type Provider struct { | ||
RegistrationState *string `json:"registrationState,omitempty"` | ||
} | ||
var provider Provider | ||
|
||
err = autorest.Respond( | ||
resp, | ||
WithErrorUnlessStatusCode(http.StatusOK), | ||
autorest.ByUnmarshallingJSON(&provider), | ||
autorest.ByClosing(), | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// poll for registered provisioning state | ||
now := time.Now() | ||
for err == nil && time.Since(now) < client.PollingDuration { | ||
// taken from the resources SDK | ||
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L45 | ||
preparer := autorest.CreatePreparer( | ||
autorest.AsGet(), | ||
autorest.WithBaseURL(newURL.String()), | ||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}", pathParameters), | ||
autorest.WithQueryParameters(queryParameters), | ||
) | ||
req, err = preparer.Prepare(&http.Request{}) | ||
if err != nil { | ||
return err | ||
} | ||
req.Cancel = originalReq.Cancel | ||
|
||
resp, err := autorest.SendWithSender(client.Sender, req, | ||
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...), | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = autorest.Respond( | ||
resp, | ||
WithErrorUnlessStatusCode(http.StatusOK), | ||
autorest.ByUnmarshallingJSON(&provider), | ||
autorest.ByClosing(), | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if provider.RegistrationState != nil && | ||
*provider.RegistrationState == "Registered" { | ||
break | ||
} | ||
|
||
delayed := autorest.DelayWithRetryAfter(resp, originalReq.Cancel) | ||
if !delayed { | ||
autorest.DelayForBackoff(client.PollingDelay, 0, originalReq.Cancel) | ||
} | ||
} | ||
if !(time.Since(now) < client.PollingDuration) { | ||
return errors.New("polling for resource provider registration has exceeded the polling duration") | ||
} | ||
return err | ||
} | ||
|
||
func getSubscription(path string) string { | ||
parts := strings.Split(path, "/") | ||
for i, v := range parts { | ||
if v == "subscriptions" && (i+1) < len(parts) { | ||
return parts[i+1] | ||
} | ||
} | ||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package azure | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
"time" | ||
|
||
"github.com/Azure/go-autorest/autorest" | ||
"github.com/Azure/go-autorest/autorest/mocks" | ||
) | ||
|
||
func TestDoRetryWithRegistration(t *testing.T) { | ||
client := mocks.NewSender() | ||
// first response, should retry because it is a transient error | ||
client.AppendResponse(mocks.NewResponseWithStatus("Internal server error", http.StatusInternalServerError)) | ||
// response indicates the resource provider has not been registered | ||
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{ | ||
"error":{ | ||
"code":"MissingSubscriptionRegistration", | ||
"message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.EventGrid'. See https://aka.ms/rps-not-found for how to register subscriptions.", | ||
"details":[ | ||
{ | ||
"code":"MissingSubscriptionRegistration", | ||
"target":"Microsoft.EventGrid", | ||
"message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.EventGrid'. See https://aka.ms/rps-not-found for how to register subscriptions." | ||
} | ||
] | ||
} | ||
} | ||
`), http.StatusConflict, "MissingSubscriptionRegistration")) | ||
// first poll response, still not ready | ||
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{ | ||
"registrationState": "Registering" | ||
} | ||
`), http.StatusOK, "200 OK")) | ||
// last poll response, respurce provider has been registered | ||
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{ | ||
"registrationState": "Registered" | ||
} | ||
`), http.StatusOK, "200 OK")) | ||
// retry original request, response is successful | ||
client.AppendResponse(mocks.NewResponseWithStatus("200 OK", http.StatusOK)) | ||
|
||
req := mocks.NewRequestForURL("https://lol/subscriptions/rofl") | ||
req.Body = mocks.NewBody("lolol") | ||
r, err := autorest.SendWithSender(client, req, | ||
DoRetryWithRegistration(autorest.Client{ | ||
PollingDelay: time.Second, | ||
PollingDuration: time.Second * 10, | ||
RetryAttempts: 5, | ||
RetryDuration: time.Second, | ||
Sender: client, | ||
}), | ||
) | ||
if err != nil { | ||
t.Fatalf("got error: %v", err) | ||
} | ||
|
||
autorest.Respond(r, | ||
autorest.ByDiscardingBody(), | ||
autorest.ByClosing(), | ||
) | ||
|
||
if r.StatusCode != http.StatusOK { | ||
t.Fatalf("azure: Sender#DoRetryWithRegistration -- Got: StatusCode %v; Want: StatusCode 200 OK", r.StatusCode) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters