-
Notifications
You must be signed in to change notification settings - Fork 837
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* LRO poller rewrite Simplified implementation of LRO pollers for ARM. Public surface area has been slightly changed, making it identical to the data-plane implementation. The different polling mechanisms have been split into internal packages, with an exported LROPoller that implements the overall polling algorithm. * Fix NopCloser package for earlier versions of Go * handle empty response when a model is provided log a message in this case * update location polling URL * fix final GET for POST * handle absense of provisioning state for initial response body polling * verify polling URLs * move checking of errors to Poll method * move logging of status to Poll() * export pipeline for pager-poller scenario * fail on a 202 DELETE/POST with no polling URL * differentiate between no response body and missing states this is important for body polling which can tolerate a missing response body for certain status codes. * relax provisioning state requirement on initial async PUT response * use provisioningState for loc polling when available * fix up token decoding * consolidate status constants * fix code comment * fix closing of response body * add comments to fields * simplify error handling for missing states * add check for empty Location header
- Loading branch information
1 parent
7c0aeb2
commit 564971c
Showing
13 changed files
with
1,627 additions
and
952 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// +build go1.13 | ||
|
||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package async | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/Azure/azure-sdk-for-go/sdk/armcore/internal/pollers" | ||
"github.com/Azure/azure-sdk-for-go/sdk/azcore" | ||
) | ||
|
||
const ( | ||
finalStateAsync = "azure-async-operation" | ||
finalStateLoc = "location" | ||
finalStateOrig = "original-uri" | ||
) | ||
|
||
// Applicable returns true if the LRO is using Azure-AsyncOperation. | ||
func Applicable(resp *azcore.Response) bool { | ||
return resp.Header.Get(pollers.HeaderAzureAsync) != "" | ||
} | ||
|
||
// Poller is an LRO poller that uses the Azure-AsyncOperation pattern. | ||
type Poller struct { | ||
// The poller's type, used for resume token processing. | ||
Type string `json:"type"` | ||
|
||
// The URL from Azure-AsyncOperation header. | ||
AsyncURL string `json:"asyncURL"` | ||
|
||
// The URL from Location header. | ||
LocURL string `json:"locURL"` | ||
|
||
// The URL from the initial LRO request. | ||
OrigURL string `json:"origURL"` | ||
|
||
// The HTTP method from the initial LRO request. | ||
Method string `json:"method"` | ||
|
||
// The value of final-state-via from swagger, can be the empty string. | ||
FinalState string `json:"finalState"` | ||
|
||
// The LRO's current state. | ||
CurState string `json:"state"` | ||
} | ||
|
||
// New creates a new Poller from the provided initial response and final-state type. | ||
func New(resp *azcore.Response, finalState string, pollerID string) (*Poller, error) { | ||
azcore.Log().Write(azcore.LogLongRunningOperation, "Using Azure-AsyncOperation poller.") | ||
asyncURL := resp.Header.Get(pollers.HeaderAzureAsync) | ||
if asyncURL == "" { | ||
return nil, errors.New("response is missing Azure-AsyncOperation header") | ||
} | ||
if !pollers.IsValidURL(asyncURL) { | ||
return nil, fmt.Errorf("invalid polling URL %s", asyncURL) | ||
} | ||
p := &Poller{ | ||
Type: pollers.MakeID(pollerID, "async"), | ||
AsyncURL: asyncURL, | ||
LocURL: resp.Header.Get(pollers.HeaderLocation), | ||
OrigURL: resp.Request.URL.String(), | ||
Method: resp.Request.Method, | ||
FinalState: finalState, | ||
} | ||
// check for provisioning state | ||
state, err := pollers.GetProvisioningState(resp) | ||
if errors.Is(err, pollers.ErrNoBody) || state == "" { | ||
// NOTE: the ARM RPC spec explicitly states that for async PUT the initial response MUST | ||
// contain a provisioning state. to maintain compat with track 1 and other implementations | ||
// we are explicitly relaxing this requirement. | ||
/*if resp.Request.Method == http.MethodPut { | ||
// initial response for a PUT requires a provisioning state | ||
return nil, err | ||
}*/ | ||
// for DELETE/PATCH/POST, provisioning state is optional | ||
state = pollers.StatusInProgress | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
p.CurState = state | ||
return p, nil | ||
} | ||
|
||
// Done returns true if the LRO has reached a terminal state. | ||
func (p *Poller) Done() bool { | ||
return pollers.IsTerminalState(p.Status()) | ||
} | ||
|
||
// Update updates the Poller from the polling response. | ||
func (p *Poller) Update(resp *azcore.Response) error { | ||
state, err := pollers.GetStatus(resp) | ||
if err != nil { | ||
return err | ||
} else if state == "" { | ||
return errors.New("the response did not contain a status") | ||
} | ||
p.CurState = state | ||
return nil | ||
} | ||
|
||
// FinalGetURL returns the URL to perform a final GET for the payload, or the empty string if not required. | ||
func (p *Poller) FinalGetURL() string { | ||
if p.Method == http.MethodPatch || p.Method == http.MethodPut { | ||
// for PATCH and PUT, the final GET is on the original resource URL | ||
return p.OrigURL | ||
} else if p.Method == http.MethodPost { | ||
if p.FinalState == finalStateAsync { | ||
return "" | ||
} else if p.FinalState == finalStateOrig { | ||
return p.OrigURL | ||
} else if p.LocURL != "" { | ||
// ideally FinalState would be set to "location" but it isn't always. | ||
// must check last due to more permissive condition. | ||
return p.LocURL | ||
} | ||
} | ||
return "" | ||
} | ||
|
||
// URL returns the polling URL. | ||
func (p *Poller) URL() string { | ||
return p.AsyncURL | ||
} | ||
|
||
// Status returns the status of the LRO. | ||
func (p *Poller) Status() string { | ||
return p.CurState | ||
} |
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,186 @@ | ||
// +build go1.13 | ||
|
||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package async | ||
|
||
import ( | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/Azure/azure-sdk-for-go/sdk/armcore/internal/pollers" | ||
"github.com/Azure/azure-sdk-for-go/sdk/azcore" | ||
) | ||
|
||
const ( | ||
fakePollingURL = "https://foo.bar.baz/status" | ||
fakeResourceURL = "https://foo.bar.baz/resource" | ||
) | ||
|
||
func initialResponse(method string, resp io.Reader) *azcore.Response { | ||
req, err := http.NewRequest(method, fakeResourceURL, nil) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return &azcore.Response{ | ||
Response: &http.Response{ | ||
Body: ioutil.NopCloser(resp), | ||
Header: http.Header{}, | ||
Request: req, | ||
}, | ||
} | ||
} | ||
|
||
func pollingResponse(resp io.Reader) *azcore.Response { | ||
return &azcore.Response{ | ||
Response: &http.Response{ | ||
Body: ioutil.NopCloser(resp), | ||
Header: http.Header{}, | ||
}, | ||
} | ||
} | ||
|
||
func TestApplicable(t *testing.T) { | ||
resp := azcore.Response{ | ||
Response: &http.Response{ | ||
Header: http.Header{}, | ||
}, | ||
} | ||
if Applicable(&resp) { | ||
t.Fatal("missing Azure-AsyncOperation should not be applicable") | ||
} | ||
resp.Response.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
if !Applicable(&resp) { | ||
t.Fatal("having Azure-AsyncOperation should be applicable") | ||
} | ||
} | ||
|
||
func TestNew(t *testing.T) { | ||
const jsonBody = `{ "properties": { "provisioningState": "Started" } }` | ||
resp := initialResponse(http.MethodPut, strings.NewReader(jsonBody)) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if u := poller.FinalGetURL(); u != fakeResourceURL { | ||
t.Fatalf("unexpected final get URL %s", u) | ||
} | ||
if s := poller.Status(); s != "Started" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
if u := poller.URL(); u != fakePollingURL { | ||
t.Fatalf("unexpected polling URL %s", u) | ||
} | ||
if err := poller.Update(pollingResponse(strings.NewReader(`{ "status": "InProgress" }`))); err != nil { | ||
t.Fatal(err) | ||
} | ||
if s := poller.Status(); s != "InProgress" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
} | ||
|
||
func TestNewDeleteNoProvState(t *testing.T) { | ||
resp := initialResponse(http.MethodDelete, http.NoBody) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if s := poller.Status(); s != "InProgress" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
} | ||
|
||
func TestNewPutNoProvState(t *testing.T) { | ||
// missing provisioning state on initial response | ||
// NOTE: ARM RPC forbids this but we allow it for back-compat | ||
resp := initialResponse(http.MethodPut, http.NoBody) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if s := poller.Status(); s != "InProgress" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
} | ||
|
||
func TestNewFinalGetLocation(t *testing.T) { | ||
const ( | ||
jsonBody = `{ "properties": { "provisioningState": "Started" } }` | ||
locURL = "https://foo.bar.baz/location" | ||
) | ||
resp := initialResponse(http.MethodPost, strings.NewReader(jsonBody)) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
resp.Header.Set(pollers.HeaderLocation, locURL) | ||
poller, err := New(resp, "location", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if u := poller.FinalGetURL(); u != locURL { | ||
t.Fatalf("unexpected final get URL %s", u) | ||
} | ||
if u := poller.URL(); u != fakePollingURL { | ||
t.Fatalf("unexpected polling URL %s", u) | ||
} | ||
} | ||
|
||
func TestNewFinalGetOrigin(t *testing.T) { | ||
const ( | ||
jsonBody = `{ "properties": { "provisioningState": "Started" } }` | ||
locURL = "https://foo.bar.baz/location" | ||
) | ||
resp := initialResponse(http.MethodPost, strings.NewReader(jsonBody)) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
resp.Header.Set(pollers.HeaderLocation, locURL) | ||
poller, err := New(resp, "original-uri", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if u := poller.FinalGetURL(); u != fakeResourceURL { | ||
t.Fatalf("unexpected final get URL %s", u) | ||
} | ||
if u := poller.URL(); u != fakePollingURL { | ||
t.Fatalf("unexpected polling URL %s", u) | ||
} | ||
} | ||
|
||
func TestNewPutNoProvStateOnUpdate(t *testing.T) { | ||
// missing provisioning state on initial response | ||
// NOTE: ARM RPC forbids this but we allow it for back-compat | ||
resp := initialResponse(http.MethodPut, http.NoBody) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if s := poller.Status(); s != "InProgress" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
if err := poller.Update(pollingResponse(strings.NewReader("{}"))); err == nil { | ||
t.Fatal("unexpected nil error") | ||
} | ||
} |
Oops, something went wrong.