Skip to content
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

LRO poller rewrite #14752

Merged
merged 21 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sdk/armcore/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ module github.com/Azure/azure-sdk-for-go/sdk/armcore
go 1.14

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1
)
9 changes: 4 additions & 5 deletions sdk/armcore/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0 h1:4HBTI/9UDZN7tsXyB5TYP3xCv5xVHIUTbvHHH2HFxQY=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0 h1:HG1ggl8L3ZkV/Ydanf7lKr5kkhhPGCpWdnr1J6v7cO4=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2 h1:UC4vfOhW2l0f2QOCQpOxJS4/K6oKFy2tQZE+uWU1MEo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2/go.mod h1:MVdrcUC4Hup35qHym3VdzoW+NBgBxrta9Vei97jRtM8=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1 h1:vx8McI56N5oLSQu8xa+xdiE0fjQq8W8Zt49vHP8Rygw=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -10,7 +10,6 @@ golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
133 changes: 133 additions & 0 deletions sdk/armcore/internal/pollers/async/async.go
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
}*/
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
} 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
}
186 changes: 186 additions & 0 deletions sdk/armcore/internal/pollers/async/async_test.go
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")
}
}
Loading