Skip to content

Commit

Permalink
chore: migrate transport to its own package (#2446)
Browse files Browse the repository at this point in the history
* chore: migrate transport to itw own package

* Fix linter
  • Loading branch information
remyleone committed Mar 12, 2024
1 parent 384a992 commit a1a516f
Show file tree
Hide file tree
Showing 35 changed files with 244 additions and 202 deletions.
38 changes: 28 additions & 10 deletions scaleway/retryable_transport.go → internal/transport/retry.go
@@ -1,24 +1,29 @@
package scaleway
package transport

import (
"bytes"
"context"
"errors"
"io"
"io/ioutil"
"net/http"
"time"

"github.com/hashicorp/go-retryablehttp"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/logging"
)

type retryableTransportOptions struct {
// DefaultWaitRetryInterval is used to set the retry interval to 0 during acceptance tests
var DefaultWaitRetryInterval *time.Duration

type RetryableTransportOptions struct {
RetryMax *int
RetryWaitMax *time.Duration
RetryWaitMin *time.Duration
}

func newRetryableTransportWithOptions(defaultTransport http.RoundTripper, options retryableTransportOptions) http.RoundTripper {
func NewRetryableTransportWithOptions(defaultTransport http.RoundTripper, options RetryableTransportOptions) http.RoundTripper {
c := retryablehttp.NewClient()
c.HTTPClient = &http.Client{Transport: defaultTransport}

Expand Down Expand Up @@ -55,22 +60,22 @@ func newRetryableTransportWithOptions(defaultTransport http.RoundTripper, option
c.RetryWaitMin = *options.RetryWaitMin
}

return &retryableTransport{c}
return &RetryableTransport{c}
}

// NewRetryableTransport creates a http transport with retry capability.
// TODO Retry logic should be moved in the SDK
// newRetryableTransport creates a http transport with retry capability.
func newRetryableTransport(defaultTransport http.RoundTripper) http.RoundTripper {
return newRetryableTransportWithOptions(defaultTransport, retryableTransportOptions{})
func NewRetryableTransport(defaultTransport http.RoundTripper) http.RoundTripper {
return NewRetryableTransportWithOptions(defaultTransport, RetryableTransportOptions{})
}

// client is a bridge between scw.httpClient interface and retryablehttp.Client
type retryableTransport struct {
// RetryableTransport client is a bridge between scw.httpClient interface and retryablehttp.Client
type RetryableTransport struct {
*retryablehttp.Client
}

// RoundTrip wraps calling an HTTP method with retries.
func (c *retryableTransport) RoundTrip(r *http.Request) (*http.Response, error) {
func (c *RetryableTransport) RoundTrip(r *http.Request) (*http.Response, error) {
var body io.ReadSeeker
if r.Body != nil {
bs, err := ioutil.ReadAll(r.Body)
Expand All @@ -95,3 +100,16 @@ func (c *retryableTransport) RoundTrip(r *http.Request) (*http.Response, error)
}
return c.Client.Do(req)
}

func RetryOnTransientStateError[T any, U any](action func() (T, error), waiter func() (U, error)) (T, error) { //nolint:ireturn
t, err := action()
var transientStateError *scw.TransientStateError
if errors.As(err, &transientStateError) {
_, err := waiter()
if err != nil {
return t, err
}
return RetryOnTransientStateError(action, waiter)
}
return t, err
}
63 changes: 63 additions & 0 deletions internal/transport/retry_aws.go
@@ -0,0 +1,63 @@
package transport

import (
"context"
"errors"
"time"

"github.com/hashicorp/aws-sdk-go-base/tfawserr"
)

// RetryWhenAWSErrCodeEquals retries a function when it returns a specific AWS error
func RetryWhenAWSErrCodeEquals[T any](ctx context.Context, codes []string, config *RetryWhenConfig[T]) (T, error) { //nolint: ireturn
return retryWhen(ctx, config, func(err error) bool {
return tfawserr.ErrCodeEquals(err, codes...)
})
}

// RetryWhenAWSErrCodeNotEquals retries a function until it returns a specific AWS error
func RetryWhenAWSErrCodeNotEquals[T any](ctx context.Context, codes []string, config *RetryWhenConfig[T]) (T, error) { //nolint: ireturn
return retryWhen(ctx, config, func(err error) bool {
if err == nil {
return true
}

return !tfawserr.ErrCodeEquals(err, codes...)
})
}

// retryWhen executes the function passed in the configuration object until the timeout is reached or the context is cancelled.
// It will retry if the shouldRetry function returns true. It will stop if the shouldRetry function returns false.
func retryWhen[T any](ctx context.Context, config *RetryWhenConfig[T], shouldRetry func(error) bool) (T, error) { //nolint: ireturn
retryInterval := config.Interval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
}

timer := time.NewTimer(config.Timeout)

for {
result, err := config.Function()
if shouldRetry(err) {
select {
case <-timer.C:
return result, ErrRetryWhenTimeout
case <-ctx.Done():
return result, ctx.Err()
default:
time.Sleep(retryInterval) // lintignore:R018
continue
}
}

return result, err
}
}

type RetryWhenConfig[T any] struct {
Timeout time.Duration
Interval time.Duration
Function func() (T, error)
}

var ErrRetryWhenTimeout = errors.New("timeout reached")
71 changes: 0 additions & 71 deletions scaleway/helpers.go
Expand Up @@ -14,7 +14,6 @@ import (
"testing"
"time"

"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand All @@ -32,9 +31,6 @@ const (
EndpointsID = ServiceName // ID to look up a service endpoint with.
)

// DefaultWaitRetryInterval is used to set the retry interval to 0 during acceptance tests
var DefaultWaitRetryInterval *time.Duration

// RegionalID represents an ID that is linked with a region, eg fr-par/11111111-1111-1111-1111-111111111111
type RegionalID struct {
ID string
Expand Down Expand Up @@ -945,19 +941,6 @@ func validateMapKeyLowerCase() schema.SchemaValidateDiagFunc {
}
}

func retryOnTransientStateError[T any, U any](action func() (T, error), waiter func() (U, error)) (T, error) { //nolint:ireturn
t, err := action()
var transientStateError *scw.TransientStateError
if errors.As(err, &transientStateError) {
_, err := waiter()
if err != nil {
return t, err
}
return retryOnTransientStateError(action, waiter)
}
return t, err
}

// compareLocalities compare two localities
// They are equal if they are the same or if one is a zone contained in a region
func compareLocalities(loc1, loc2 string) bool {
Expand Down Expand Up @@ -1104,60 +1087,6 @@ func NotFound(err error) bool {
return errors.As(err, &e)
}

type RetryWhenConfig[T any] struct {
Timeout time.Duration
Interval time.Duration
Function func() (T, error)
}

var ErrRetryWhenTimeout = errors.New("timeout reached")

// retryWhen executes the function passed in the configuration object until the timeout is reached or the context is cancelled.
// It will retry if the shouldRetry function returns true. It will stop if the shouldRetry function returns false.
func retryWhen[T any](ctx context.Context, config *RetryWhenConfig[T], shouldRetry func(error) bool) (T, error) { //nolint: ireturn
retryInterval := config.Interval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
}

timer := time.NewTimer(config.Timeout)

for {
result, err := config.Function()
if shouldRetry(err) {
select {
case <-timer.C:
return result, ErrRetryWhenTimeout
case <-ctx.Done():
return result, ctx.Err()
default:
time.Sleep(retryInterval) // lintignore:R018
continue
}
}

return result, err
}
}

// retryWhenAWSErrCodeEquals retries a function when it returns a specific AWS error
func retryWhenAWSErrCodeEquals[T any](ctx context.Context, codes []string, config *RetryWhenConfig[T]) (T, error) { //nolint: ireturn
return retryWhen(ctx, config, func(err error) bool {
return tfawserr.ErrCodeEquals(err, codes...)
})
}

// retryWhenAWSErrCodeNotEquals retries a function until it returns a specific AWS error
func retryWhenAWSErrCodeNotEquals[T any](ctx context.Context, codes []string, config *RetryWhenConfig[T]) (T, error) { //nolint: ireturn
return retryWhen(ctx, config, func(err error) bool {
if err == nil {
return true
}

return !tfawserr.ErrCodeEquals(err, codes...)
})
}

func sliceContainsString(slice []string, str string) bool {
for _, v := range slice {
if v == str {
Expand Down
5 changes: 3 additions & 2 deletions scaleway/helpers_apple_silicon.go
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
applesilicon "github.com/scaleway/scaleway-sdk-go/api/applesilicon/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/transport"
)

const (
Expand Down Expand Up @@ -40,8 +41,8 @@ func asAPIWithZoneAndID(m interface{}, id string) (*applesilicon.API, scw.Zone,

func waitForAppleSiliconServer(ctx context.Context, api *applesilicon.API, zone scw.Zone, serverID string, timeout time.Duration) (*applesilicon.Server, error) {
retryInterval := defaultAppleSiliconServerRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

server, err := api.WaitForServer(&applesilicon.WaitForServerRequest{
Expand Down
17 changes: 9 additions & 8 deletions scaleway/helpers_baremetal.go
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/scaleway/scaleway-sdk-go/api/baremetal/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/transport"
)

const (
Expand Down Expand Up @@ -260,8 +261,8 @@ func detachAllPrivateNetworkFromBaremetal(ctx context.Context, d *schema.Resourc

func waitForBaremetalServer(ctx context.Context, api *baremetal.API, zone scw.Zone, serverID string, timeout time.Duration) (*baremetal.Server, error) {
retryInterval := baremetalRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

server, err := api.WaitForServer(&baremetal.WaitForServerRequest{
Expand All @@ -276,8 +277,8 @@ func waitForBaremetalServer(ctx context.Context, api *baremetal.API, zone scw.Zo

func waitForBaremetalServerInstall(ctx context.Context, api *baremetal.API, zone scw.Zone, serverID string, timeout time.Duration) (*baremetal.Server, error) {
retryInterval := baremetalRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

server, err := api.WaitForServerInstall(&baremetal.WaitForServerInstallRequest{
Expand All @@ -292,8 +293,8 @@ func waitForBaremetalServerInstall(ctx context.Context, api *baremetal.API, zone

func waitForBaremetalServerOptions(ctx context.Context, api *baremetal.API, zone scw.Zone, serverID string, timeout time.Duration) (*baremetal.Server, error) {
retryInterval := baremetalRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

server, err := api.WaitForServerOptions(&baremetal.WaitForServerOptionsRequest{
Expand All @@ -308,8 +309,8 @@ func waitForBaremetalServerOptions(ctx context.Context, api *baremetal.API, zone

func waitForBaremetalServerPrivateNetwork(ctx context.Context, api *baremetal.PrivateNetworkAPI, zone scw.Zone, serverID string, timeout time.Duration) ([]*baremetal.ServerPrivateNetwork, error) {
retryInterval := baremetalRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}
serverPrivateNetwork, err := api.WaitForServerPrivateNetworks(&baremetal.WaitForServerPrivateNetworksRequest{
Zone: zone,
Expand Down
9 changes: 5 additions & 4 deletions scaleway/helpers_block.go
Expand Up @@ -9,6 +9,7 @@ import (
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/transport"
)

const (
Expand Down Expand Up @@ -44,8 +45,8 @@ func blockAPIWithZoneAndID(m interface{}, zonedID string) (*block.API, scw.Zone,

func waitForBlockVolume(ctx context.Context, blockAPI *block.API, zone scw.Zone, id string, timeout time.Duration) (*block.Volume, error) {
retryInterval := defaultFunctionRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

volume, err := blockAPI.WaitForVolumeAndReferences(&block.WaitForVolumeAndReferencesRequest{
Expand All @@ -71,8 +72,8 @@ func customDiffCannotShrink(key string) schema.CustomizeDiffFunc {

func waitForBlockSnapshot(ctx context.Context, blockAPI *block.API, zone scw.Zone, id string, timeout time.Duration) (*block.Snapshot, error) {
retryInterval := defaultFunctionRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

snapshot, err := blockAPI.WaitForSnapshot(&block.WaitForSnapshotRequest{
Expand Down
5 changes: 3 additions & 2 deletions scaleway/helpers_cockpit.go
Expand Up @@ -9,6 +9,7 @@ import (

cockpit "github.com/scaleway/scaleway-sdk-go/api/cockpit/v1beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/transport"
)

const (
Expand Down Expand Up @@ -117,8 +118,8 @@ func flattenCockpitTokenScopes(scopes *cockpit.TokenScopes) []map[string]interfa

func waitForCockpit(ctx context.Context, api *cockpit.API, projectID string, timeout time.Duration) (*cockpit.Cockpit, error) {
retryInterval := defaultContainerRetryInterval
if DefaultWaitRetryInterval != nil {
retryInterval = *DefaultWaitRetryInterval
if transport.DefaultWaitRetryInterval != nil {
retryInterval = *transport.DefaultWaitRetryInterval
}

return api.WaitForCockpit(&cockpit.WaitForCockpitRequest{
Expand Down

0 comments on commit a1a516f

Please sign in to comment.