forked from TrueCloudLab/distribution
This upgrade, and vendors aws-sdk-go to version v1.12.36. This is because it has new API calls accessible to the S3 client, specifically S3.ListObjectsV2PagesWithContext Signed-off-by: Sargun Dhillon <sargun@sargun.me>
295 lines
8.8 KiB
Go
295 lines
8.8 KiB
Go
package request
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/aws/awsutil"
|
|
)
|
|
|
|
// WaiterResourceNotReadyErrorCode is the error code returned by a waiter when
|
|
// the waiter's max attempts have been exhausted.
|
|
const WaiterResourceNotReadyErrorCode = "ResourceNotReady"
|
|
|
|
// A WaiterOption is a function that will update the Waiter value's fields to
|
|
// configure the waiter.
|
|
type WaiterOption func(*Waiter)
|
|
|
|
// WithWaiterMaxAttempts returns the maximum number of times the waiter should
|
|
// attempt to check the resource for the target state.
|
|
func WithWaiterMaxAttempts(max int) WaiterOption {
|
|
return func(w *Waiter) {
|
|
w.MaxAttempts = max
|
|
}
|
|
}
|
|
|
|
// WaiterDelay will return a delay the waiter should pause between attempts to
|
|
// check the resource state. The passed in attempt is the number of times the
|
|
// Waiter has checked the resource state.
|
|
//
|
|
// Attempt is the number of attempts the Waiter has made checking the resource
|
|
// state.
|
|
type WaiterDelay func(attempt int) time.Duration
|
|
|
|
// ConstantWaiterDelay returns a WaiterDelay that will always return a constant
|
|
// delay the waiter should use between attempts. It ignores the number of
|
|
// attempts made.
|
|
func ConstantWaiterDelay(delay time.Duration) WaiterDelay {
|
|
return func(attempt int) time.Duration {
|
|
return delay
|
|
}
|
|
}
|
|
|
|
// WithWaiterDelay will set the Waiter to use the WaiterDelay passed in.
|
|
func WithWaiterDelay(delayer WaiterDelay) WaiterOption {
|
|
return func(w *Waiter) {
|
|
w.Delay = delayer
|
|
}
|
|
}
|
|
|
|
// WithWaiterLogger returns a waiter option to set the logger a waiter
|
|
// should use to log warnings and errors to.
|
|
func WithWaiterLogger(logger aws.Logger) WaiterOption {
|
|
return func(w *Waiter) {
|
|
w.Logger = logger
|
|
}
|
|
}
|
|
|
|
// WithWaiterRequestOptions returns a waiter option setting the request
|
|
// options for each request the waiter makes. Appends to waiter's request
|
|
// options already set.
|
|
func WithWaiterRequestOptions(opts ...Option) WaiterOption {
|
|
return func(w *Waiter) {
|
|
w.RequestOptions = append(w.RequestOptions, opts...)
|
|
}
|
|
}
|
|
|
|
// A Waiter provides the functionality to perform a blocking call which will
|
|
// wait for a resource state to be satisfied by a service.
|
|
//
|
|
// This type should not be used directly. The API operations provided in the
|
|
// service packages prefixed with "WaitUntil" should be used instead.
|
|
type Waiter struct {
|
|
Name string
|
|
Acceptors []WaiterAcceptor
|
|
Logger aws.Logger
|
|
|
|
MaxAttempts int
|
|
Delay WaiterDelay
|
|
|
|
RequestOptions []Option
|
|
NewRequest func([]Option) (*Request, error)
|
|
SleepWithContext func(aws.Context, time.Duration) error
|
|
}
|
|
|
|
// ApplyOptions updates the waiter with the list of waiter options provided.
|
|
func (w *Waiter) ApplyOptions(opts ...WaiterOption) {
|
|
for _, fn := range opts {
|
|
fn(w)
|
|
}
|
|
}
|
|
|
|
// WaiterState are states the waiter uses based on WaiterAcceptor definitions
|
|
// to identify if the resource state the waiter is waiting on has occurred.
|
|
type WaiterState int
|
|
|
|
// String returns the string representation of the waiter state.
|
|
func (s WaiterState) String() string {
|
|
switch s {
|
|
case SuccessWaiterState:
|
|
return "success"
|
|
case FailureWaiterState:
|
|
return "failure"
|
|
case RetryWaiterState:
|
|
return "retry"
|
|
default:
|
|
return "unknown waiter state"
|
|
}
|
|
}
|
|
|
|
// States the waiter acceptors will use to identify target resource states.
|
|
const (
|
|
SuccessWaiterState WaiterState = iota // waiter successful
|
|
FailureWaiterState // waiter failed
|
|
RetryWaiterState // waiter needs to be retried
|
|
)
|
|
|
|
// WaiterMatchMode is the mode that the waiter will use to match the WaiterAcceptor
|
|
// definition's Expected attribute.
|
|
type WaiterMatchMode int
|
|
|
|
// Modes the waiter will use when inspecting API response to identify target
|
|
// resource states.
|
|
const (
|
|
PathAllWaiterMatch WaiterMatchMode = iota // match on all paths
|
|
PathWaiterMatch // match on specific path
|
|
PathAnyWaiterMatch // match on any path
|
|
PathListWaiterMatch // match on list of paths
|
|
StatusWaiterMatch // match on status code
|
|
ErrorWaiterMatch // match on error
|
|
)
|
|
|
|
// String returns the string representation of the waiter match mode.
|
|
func (m WaiterMatchMode) String() string {
|
|
switch m {
|
|
case PathAllWaiterMatch:
|
|
return "pathAll"
|
|
case PathWaiterMatch:
|
|
return "path"
|
|
case PathAnyWaiterMatch:
|
|
return "pathAny"
|
|
case PathListWaiterMatch:
|
|
return "pathList"
|
|
case StatusWaiterMatch:
|
|
return "status"
|
|
case ErrorWaiterMatch:
|
|
return "error"
|
|
default:
|
|
return "unknown waiter match mode"
|
|
}
|
|
}
|
|
|
|
// WaitWithContext will make requests for the API operation using NewRequest to
|
|
// build API requests. The request's response will be compared against the
|
|
// Waiter's Acceptors to determine the successful state of the resource the
|
|
// waiter is inspecting.
|
|
//
|
|
// The passed in context must not be nil. If it is nil a panic will occur. The
|
|
// Context will be used to cancel the waiter's pending requests and retry delays.
|
|
// Use aws.BackgroundContext if no context is available.
|
|
//
|
|
// The waiter will continue until the target state defined by the Acceptors,
|
|
// or the max attempts expires.
|
|
//
|
|
// Will return the WaiterResourceNotReadyErrorCode error code if the waiter's
|
|
// retryer ShouldRetry returns false. This normally will happen when the max
|
|
// wait attempts expires.
|
|
func (w Waiter) WaitWithContext(ctx aws.Context) error {
|
|
|
|
for attempt := 1; ; attempt++ {
|
|
req, err := w.NewRequest(w.RequestOptions)
|
|
if err != nil {
|
|
waiterLogf(w.Logger, "unable to create request %v", err)
|
|
return err
|
|
}
|
|
req.Handlers.Build.PushBack(MakeAddToUserAgentFreeFormHandler("Waiter"))
|
|
err = req.Send()
|
|
|
|
// See if any of the acceptors match the request's response, or error
|
|
for _, a := range w.Acceptors {
|
|
if matched, matchErr := a.match(w.Name, w.Logger, req, err); matched {
|
|
return matchErr
|
|
}
|
|
}
|
|
|
|
// The Waiter should only check the resource state MaxAttempts times
|
|
// This is here instead of in the for loop above to prevent delaying
|
|
// unnecessary when the waiter will not retry.
|
|
if attempt == w.MaxAttempts {
|
|
break
|
|
}
|
|
|
|
// Delay to wait before inspecting the resource again
|
|
delay := w.Delay(attempt)
|
|
if sleepFn := req.Config.SleepDelay; sleepFn != nil {
|
|
// Support SleepDelay for backwards compatibility and testing
|
|
sleepFn(delay)
|
|
} else {
|
|
sleepCtxFn := w.SleepWithContext
|
|
if sleepCtxFn == nil {
|
|
sleepCtxFn = aws.SleepWithContext
|
|
}
|
|
|
|
if err := sleepCtxFn(ctx, delay); err != nil {
|
|
return awserr.New(CanceledErrorCode, "waiter context canceled", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return awserr.New(WaiterResourceNotReadyErrorCode, "exceeded wait attempts", nil)
|
|
}
|
|
|
|
// A WaiterAcceptor provides the information needed to wait for an API operation
|
|
// to complete.
|
|
type WaiterAcceptor struct {
|
|
State WaiterState
|
|
Matcher WaiterMatchMode
|
|
Argument string
|
|
Expected interface{}
|
|
}
|
|
|
|
// match returns if the acceptor found a match with the passed in request
|
|
// or error. True is returned if the acceptor made a match, error is returned
|
|
// if there was an error attempting to perform the match.
|
|
func (a *WaiterAcceptor) match(name string, l aws.Logger, req *Request, err error) (bool, error) {
|
|
result := false
|
|
var vals []interface{}
|
|
|
|
switch a.Matcher {
|
|
case PathAllWaiterMatch, PathWaiterMatch:
|
|
// Require all matches to be equal for result to match
|
|
vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
|
|
if len(vals) == 0 {
|
|
break
|
|
}
|
|
result = true
|
|
for _, val := range vals {
|
|
if !awsutil.DeepEqual(val, a.Expected) {
|
|
result = false
|
|
break
|
|
}
|
|
}
|
|
case PathAnyWaiterMatch:
|
|
// Only a single match needs to equal for the result to match
|
|
vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
|
|
for _, val := range vals {
|
|
if awsutil.DeepEqual(val, a.Expected) {
|
|
result = true
|
|
break
|
|
}
|
|
}
|
|
case PathListWaiterMatch:
|
|
// ignored matcher
|
|
case StatusWaiterMatch:
|
|
s := a.Expected.(int)
|
|
result = s == req.HTTPResponse.StatusCode
|
|
case ErrorWaiterMatch:
|
|
if aerr, ok := err.(awserr.Error); ok {
|
|
result = aerr.Code() == a.Expected.(string)
|
|
}
|
|
default:
|
|
waiterLogf(l, "WARNING: Waiter %s encountered unexpected matcher: %s",
|
|
name, a.Matcher)
|
|
}
|
|
|
|
if !result {
|
|
// If there was no matching result found there is nothing more to do
|
|
// for this response, retry the request.
|
|
return false, nil
|
|
}
|
|
|
|
switch a.State {
|
|
case SuccessWaiterState:
|
|
// waiter completed
|
|
return true, nil
|
|
case FailureWaiterState:
|
|
// Waiter failure state triggered
|
|
return true, awserr.New(WaiterResourceNotReadyErrorCode,
|
|
"failed waiting for successful resource state", err)
|
|
case RetryWaiterState:
|
|
// clear the error and retry the operation
|
|
return false, nil
|
|
default:
|
|
waiterLogf(l, "WARNING: Waiter %s encountered unexpected state: %s",
|
|
name, a.State)
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func waiterLogf(logger aws.Logger, msg string, args ...interface{}) {
|
|
if logger != nil {
|
|
logger.Log(fmt.Sprintf(msg, args...))
|
|
}
|
|
}
|