-
Notifications
You must be signed in to change notification settings - Fork 952
LRO poller rewrite #14752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
LRO poller rewrite #14752
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f54f4fc
LRO poller rewrite
jhendrixMSFT a42cca6
Fix NopCloser package for earlier versions of Go
jhendrixMSFT 5cca901
handle empty response when a model is provided
jhendrixMSFT 1de83a6
update location polling URL
jhendrixMSFT f0055bd
fix final GET for POST
jhendrixMSFT 72a12ea
handle absense of provisioning state for initial response body polling
jhendrixMSFT c23a499
verify polling URLs
jhendrixMSFT d62dc84
move checking of errors to Poll method
jhendrixMSFT 111758a
move logging of status to Poll()
jhendrixMSFT b35dcff
export pipeline for pager-poller scenario
jhendrixMSFT 9eea802
fail on a 202 DELETE/POST with no polling URL
jhendrixMSFT ab32e4c
differentiate between no response body and missing states
jhendrixMSFT 24e29f9
relax provisioning state requirement on initial async PUT response
jhendrixMSFT 216a0e5
use provisioningState for loc polling when available
jhendrixMSFT 1c94713
fix up token decoding
jhendrixMSFT 311af89
consolidate status constants
jhendrixMSFT 7d4da50
fix code comment
jhendrixMSFT f38016c
fix closing of response body
jhendrixMSFT 5578d4f
add comments to fields
jhendrixMSFT a7ffaa0
simplify error handling for missing states
jhendrixMSFT b693086
add check for empty Location header
jhendrixMSFT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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,118 @@ | ||
| // +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 { | ||
| Type string `json:"type"` | ||
| AsyncURL string `json:"asyncURL"` | ||
| LocURL string `json:"locURL"` | ||
| OrigURL string `json:"origURL"` | ||
| Method string `json:"method"` | ||
| FinalState string `json:"finalState"` | ||
| 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) || errors.Is(err, pollers.ErrNoProvisioningState) { | ||
| // 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 | ||
| }*/ | ||
catalinaperalta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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 | ||
| } | ||
| 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 hidden or 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,166 @@ | ||
| // +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) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.