Skip to content

Commit

Permalink
Merge pull request #339 from hashicorp/b/polling-uri
Browse files Browse the repository at this point in the history
SDK: updating the  pollers for `resourcemanager`
  • Loading branch information
tombuildsstuff authored Feb 28, 2023
2 parents 4c0ea3e + 939f0e9 commit 611f835
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 6 deletions.
14 changes: 13 additions & 1 deletion sdk/client/resourcemanager/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,25 @@ func PollerFromResponse(response *client.Response, client *Client) (poller polle
strings.EqualFold(response.Request.Method, "POST") ||
strings.EqualFold(response.Request.Method, "PUT")
if statusCodesToCheckProvisioningState && contentTypeMatchesForProvisioningStateCheck && methodIsApplicable {
provisioningState, provisioningStateErr := provisioningStatePollerFromResponse(response, client, DefaultProvisioningStatePollingInterval)
provisioningState, provisioningStateErr := provisioningStatePollerFromResponse(response, client, DefaultPollingInterval)
if provisioningStateErr != nil {
err = provisioningStateErr
return pollers.Poller{}, fmt.Errorf("building provisioningState poller: %+v", provisioningStateErr)
}
return pollers.NewPoller(provisioningState, provisioningState.initialRetryDuration, pollers.DefaultNumberOfDroppedConnectionsToAllow), nil
}

// finally, if it was a Delete that returned a 200/204
methodIsDelete := strings.EqualFold(response.Request.Method, "DELETE")
statusCodesToCheckDelete := response.StatusCode == http.StatusOK || response.StatusCode == http.StatusNoContent
if methodIsDelete && statusCodesToCheckDelete {
deletePoller, deletePollerErr := deletePollerFromResponse(response, client, DefaultPollingInterval)
if deletePollerErr != nil {
err = deletePollerErr
return pollers.Poller{}, fmt.Errorf("building delete poller: %+v", deletePollerErr)
}
return pollers.NewPoller(deletePoller, deletePoller.initialRetryDuration, pollers.DefaultNumberOfDroppedConnectionsToAllow), nil
}

return pollers.Poller{}, fmt.Errorf("no applicable pollers were found for the response")
}
120 changes: 120 additions & 0 deletions sdk/client/resourcemanager/poller_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package resourcemanager

import (
"context"
"fmt"
"net/http"
"time"

"github.com/hashicorp/go-azure-sdk/sdk/client"
"github.com/hashicorp/go-azure-sdk/sdk/client/pollers"
"github.com/hashicorp/go-azure-sdk/sdk/odata"
)

var _ pollers.PollerType = &deletePoller{}

type deletePoller struct {
apiVersion string
client *Client
initialRetryDuration time.Duration
originalUri string
resourcePath string
}

func deletePollerFromResponse(response *client.Response, client *Client, pollingInterval time.Duration) (*deletePoller, error) {
// if we've gotten to this point then we're polling against a Resource Manager resource/operation of some kind
// we next need to determine if the current URI is a Resource Manager resource, or if we should be polling on the
// resource (e.g. `/my/resource`) rather than an operation on the resource (e.g. `/my/resource/start`)
if response.Request == nil {
return nil, fmt.Errorf("request was nil")
}
originalUri := response.Request.URL.RequestURI()
if response.Request.URL == nil {
return nil, fmt.Errorf("request url was nil")
}

// all Resource Manager operations require the `api-version` querystring
apiVersion := response.Request.URL.Query().Get("api-version")
if apiVersion == "" {
return nil, fmt.Errorf("unable to determine `api-version` from %q", originalUri)
}

resourcePath, err := resourceManagerResourcePathFromUri(originalUri)
if err != nil {
return nil, fmt.Errorf("determining Resource Manager Resource Path from %q: %+v", originalUri, err)
}

return &deletePoller{
apiVersion: apiVersion,
client: client,
initialRetryDuration: pollingInterval,
originalUri: originalUri,
resourcePath: *resourcePath,
}, nil
}

func (p deletePoller) Poll(ctx context.Context) (result *pollers.PollResult, err error) {
opts := client.RequestOptions{
ContentType: "application/json; charset=utf-8",
ExpectedStatusCodes: []int{
http.StatusOK,
http.StatusNotFound,
},
HttpMethod: http.MethodGet,
OptionsObject: deleteOptions{
apiVersion: p.apiVersion,
},
Path: p.resourcePath,
}
req, err := p.client.NewRequest(ctx, opts)
if err != nil {
return nil, fmt.Errorf("building request: %+v", err)
}
resp, err := p.client.Execute(ctx, req)
if err != nil {
return nil, fmt.Errorf("executing request: %+v", err)
}
if resp == nil {
return nil, pollers.PollingDroppedConnectionError{}
}

result = &pollers.PollResult{
PollInterval: p.initialRetryDuration,
}

if resp.Response != nil {
switch resp.StatusCode {
case http.StatusNotFound:
result.Status = pollers.PollingStatusSucceeded
return

case http.StatusOK:
result.Status = pollers.PollingStatusInProgress
return
}

err = fmt.Errorf("unexpected status code when polling for resource after deletion, expected a 200/204 but got %d", resp.StatusCode)
}

return
}

var _ client.Options = deleteOptions{}

type deleteOptions struct {
apiVersion string
}

func (p deleteOptions) ToHeaders() *client.Headers {
return &client.Headers{}
}

func (p deleteOptions) ToOData() *odata.Query {
return &odata.Query{}
}

func (p deleteOptions) ToQuery() *client.QueryParams {
q := client.QueryParams{}
q.Append("api-version", p.apiVersion)
return &q
}
22 changes: 18 additions & 4 deletions sdk/client/resourcemanager/poller_lro.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ func longRunningOperationPollerFromResponse(resp *client.Response, client *clien
return nil, fmt.Errorf("invalid polling URL %q in response: URL was not absolute", pollingUrl)
}
poller.pollingUrl = u
if endpoint, err := url.Parse(string(client.BaseUri)); err == nil && u.Host != endpoint.Host {
return nil, fmt.Errorf("unsupported polling URL %q: client endpoint is different", pollingUrl)
}

if resp.Request != nil {
poller.originalUrl = resp.Request.URL
Expand Down Expand Up @@ -100,7 +97,9 @@ func (p *longRunningOperationPoller) Poll(ctx context.Context) (result *pollers.
// Custom RetryFunc to inspect the operation payload and check the status
req.RetryFunc = client.RequestRetryAny(defaultRetryFunctions...)

result = &pollers.PollResult{}
result = &pollers.PollResult{
PollInterval: p.initialRetryDuration,
}
result.HttpResponse, err = req.Execute(ctx)
if err != nil {
return nil, err
Expand All @@ -115,6 +114,13 @@ func (p *longRunningOperationPoller) Poll(ctx context.Context) (result *pollers.
}
result.HttpResponse.Body.Close()

// update the poll interval if a Retry-After header is returned
if s, ok := result.HttpResponse.Header["Retry-After"]; ok {
if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {
result.PollInterval = time.Second * time.Duration(sleep)
}
}

// 202's don't necessarily return a body, so there's nothing to deserialize
if result.HttpResponse.StatusCode == http.StatusAccepted {
result.Status = pollers.PollingStatusInProgress
Expand Down Expand Up @@ -145,6 +151,10 @@ func (p *longRunningOperationPoller) Poll(ctx context.Context) (result *pollers.
statusFailed: pollers.PollingStatusFailed,
statusInProgress: pollers.PollingStatusInProgress,
statusSucceeded: pollers.PollingStatusSucceeded,

// whilst the standard set above should be sufficient, some APIs differ from the spec and should be documented below:
// SignalR@2022-02-01 returns `Running` rather than `InProgress` during creation
"Running": pollers.PollingStatusInProgress,
}
for k, v := range statuses {
if strings.EqualFold(string(op.Properties.ProvisioningState), string(k)) {
Expand All @@ -156,6 +166,10 @@ func (p *longRunningOperationPoller) Poll(ctx context.Context) (result *pollers.
return
}
}

if result.Status == "" {
err = fmt.Errorf("`result.Status` was nil/empty - `op.Status` was %q / `op.Properties.ProvisioningState` was %q", string(op.Status), string(op.Properties.ProvisioningState))
}
}

return
Expand Down
2 changes: 1 addition & 1 deletion sdk/client/resourcemanager/poller_provisioning_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/hashicorp/go-azure-sdk/sdk/odata"
)

const DefaultProvisioningStatePollingInterval = 2 * time.Second
const DefaultPollingInterval = 10 * time.Second

var _ pollers.PollerType = &provisioningStatePoller{}

Expand Down

0 comments on commit 611f835

Please sign in to comment.