From 2a4827f7db0e8682e68a76519e4e5bd1e84ba3d2 Mon Sep 17 00:00:00 2001 From: Roberto Losanno Date: Thu, 18 Sep 2025 13:22:42 +0100 Subject: [PATCH 1/2] Add rate limiting and retry configuration to Contentstack provider - Add rate_limit, rate_burst, and max_retries provider attributes - Update provider schema to support new rate limiting options - Add automatic retry logic integration with contentstack-go-sdk - Update provider documentation with new attributes - Add simplified examples for provider and rate-limiting configurations - Update go.mod to use enhanced contentstack-go-sdk with rate limiting This enables reliable Terraform operations by automatically handling Contentstack API rate limits (10 req/sec) with configurable throttling and exponential backoff retry logic for 429 responses. Related: contentstack-go-sdk rate limiting implementation --- .gitignore | 1 + docs/index.md | 3 +++ examples/provider/main.tf | 14 ++++++++++++++ examples/rate-limiting/main.tf | 31 +++++++++++++++++++++++++++++++ go.mod | 5 ++++- go.sum | 12 ++++++++---- internal/provider/provider.go | 31 ++++++++++++++++++++++++++----- 7 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 examples/provider/main.tf create mode 100644 examples/rate-limiting/main.tf diff --git a/.gitignore b/.gitignore index 4b68ee1..2a8b2de 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /.idea /.vscode /local +.DS_Store diff --git a/docs/index.md b/docs/index.md index 0d70631..af7a02f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,3 +22,6 @@ description: |- - `base_url` (String) The BaseURL, e.g. https://eu-api.contentstack.com/. See https://www.contentstack.com/docs/developers/apis/content-management-api/#base-url - `branch` (String) The branch to manage resources in. If not specified, the main branch will be used. - `management_token` (String, Sensitive) Management Tokens are stack-level tokens, with no users attached to them. +- `max_retries` (Number) The maximum number of retry attempts for 429 (rate limit) responses. Defaults to 3. Uses exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s. +- `rate_burst` (Number) The maximum burst size for rate limiting. Defaults to 10. This allows short bursts of requests above the rate limit. +- `rate_limit` (Number) The maximum number of requests per second to the Contentstack API. Defaults to 10.0 to comply with API limits. Set to 0 to disable rate limiting. diff --git a/examples/provider/main.tf b/examples/provider/main.tf new file mode 100644 index 0000000..1cb3e6d --- /dev/null +++ b/examples/provider/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + contentstack = { + source = "labd/contentstack" + } + } +} + +provider "contentstack" { + base_url = "https://api.contentstack.io" + api_key = "" + management_token = "" + branch = "main" +} \ No newline at end of file diff --git a/examples/rate-limiting/main.tf b/examples/rate-limiting/main.tf new file mode 100644 index 0000000..e766abd --- /dev/null +++ b/examples/rate-limiting/main.tf @@ -0,0 +1,31 @@ +terraform { + required_providers { + contentstack = { + source = "labd/contentstack" + } + } +} + +provider "contentstack" { + base_url = "https://api.contentstack.io" + api_key = "" + management_token = "" + + rate_limit = 8.0 + rate_burst = 5 + max_retries = 3 +} + +resource "contentstack_content_type" "example" { + title = "Example Content Type" + uid = "example" + + schema = jsonencode([ + { + display_name = "Title" + uid = "title" + data_type = "text" + mandatory = true + } + ]) +} \ No newline at end of file diff --git a/go.mod b/go.mod index 15c9eb8..2d7101d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/labd/terraform-provider-contentstack -go 1.18 +go 1.24.0 + +toolchain go1.24.3 require ( github.com/hashicorp/terraform-plugin-docs v0.9.0 @@ -59,6 +61,7 @@ require ( golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.13.0 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20200711021454-869866162049 // indirect google.golang.org/grpc v1.46.0 // indirect diff --git a/go.sum b/go.sum index 71ecb3d..2aa92f4 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,7 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= @@ -149,21 +150,19 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/labd/contentstack-go-sdk v0.1.0 h1:lxBbGBkIlileNn2+VrVG6XP776Llxy9i4THPBA01xTU= -github.com/labd/contentstack-go-sdk v0.1.0/go.mod h1:J17aqV8NOcmFfkkotnAljlt6Ctwlu21Ieeo/0BPLNtg= -github.com/labd/contentstack-go-sdk v0.1.0 h1:lxBbGBkIlileNn2+VrVG6XP776Llxy9i4THPBA01xTU= -github.com/labd/contentstack-go-sdk v0.1.0/go.mod h1:J17aqV8NOcmFfkkotnAljlt6Ctwlu21Ieeo/0BPLNtg= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -190,6 +189,7 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= +github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -202,11 +202,13 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -304,6 +306,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a80cfc2..758efae 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -53,17 +53,35 @@ func (p *provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) Optional: true, Description: "The branch to manage resources in. If not specified, the main branch will be used.", }, + "rate_limit": { + Type: types.Float64Type, + Optional: true, + Description: "The maximum number of requests per second to the Contentstack API. Defaults to 10.0 to comply with API limits. Set to 0 to disable rate limiting.", + }, + "rate_burst": { + Type: types.Int64Type, + Optional: true, + Description: "The maximum burst size for rate limiting. Defaults to 10. This allows short bursts of requests above the rate limit.", + }, + "max_retries": { + Type: types.Int64Type, + Optional: true, + Description: "The maximum number of retry attempts for 429 (rate limit) responses. Defaults to 3. Uses exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s.", + }, }, }, nil } // Provider schema struct type providerData struct { - BaseURL types.String `tfsdk:"base_url"` - AuthToken types.String `tfsdk:"auth_token"` - ApiKey types.String `tfsdk:"api_key"` - ManagementToken types.String `tfsdk:"management_token"` - Branch types.String `tfsdk:"branch"` + BaseURL types.String `tfsdk:"base_url"` + AuthToken types.String `tfsdk:"auth_token"` + ApiKey types.String `tfsdk:"api_key"` + ManagementToken types.String `tfsdk:"management_token"` + Branch types.String `tfsdk:"branch"` + RateLimit types.Float64 `tfsdk:"rate_limit"` + RateBurst types.Int64 `tfsdk:"rate_burst"` + MaxRetries types.Int64 `tfsdk:"max_retries"` } func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) { @@ -82,6 +100,9 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq HTTPClient: &http.Client{ Transport: management.DebugTransport, }, + RateLimit: config.RateLimit.Value, + RateBurst: int(config.RateBurst.Value), + MaxRetries: int(config.MaxRetries.Value), } c, err := management.NewClient(cfg) From a7c5564b93681152bb51028b11386629503d64a1 Mon Sep 17 00:00:00 2001 From: Roberto Losanno Date: Fri, 19 Sep 2025 12:42:53 +0100 Subject: [PATCH 2/2] feat: implement retry logic using go-retryablehttp - Add retryableRoundTripper for seamless SDK integration - Implement retry policy for 429 responses only - Add max_retries, retry_wait_min, retry_wait_max provider attributes - Update examples and documentation - Use exponential backoff with configurable wait times - Maintain compatibility with existing rate limiting --- docs/index.md | 4 +- examples/rate-limiting/main.tf | 8 ++-- go.mod | 15 +++--- go.sum | 34 ++++++++------ internal/provider/provider.go | 83 ++++++++++++++++++++++++++++++---- 5 files changed, 111 insertions(+), 33 deletions(-) diff --git a/docs/index.md b/docs/index.md index af7a02f..e58c29d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,8 @@ description: |- - `base_url` (String) The BaseURL, e.g. https://eu-api.contentstack.com/. See https://www.contentstack.com/docs/developers/apis/content-management-api/#base-url - `branch` (String) The branch to manage resources in. If not specified, the main branch will be used. - `management_token` (String, Sensitive) Management Tokens are stack-level tokens, with no users attached to them. -- `max_retries` (Number) The maximum number of retry attempts for 429 (rate limit) responses. Defaults to 3. Uses exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s. +- `max_retries` (Number) The maximum number of retry attempts for 429 (rate limit) responses. Defaults to 3. Uses exponential backoff with jitter. - `rate_burst` (Number) The maximum burst size for rate limiting. Defaults to 10. This allows short bursts of requests above the rate limit. - `rate_limit` (Number) The maximum number of requests per second to the Contentstack API. Defaults to 10.0 to comply with API limits. Set to 0 to disable rate limiting. +- `retry_wait_max` (Number) The maximum wait time in seconds between retries. Defaults to 30 seconds. +- `retry_wait_min` (Number) The minimum wait time in seconds between retries. Defaults to 1 second. diff --git a/examples/rate-limiting/main.tf b/examples/rate-limiting/main.tf index e766abd..0294920 100644 --- a/examples/rate-limiting/main.tf +++ b/examples/rate-limiting/main.tf @@ -11,9 +11,11 @@ provider "contentstack" { api_key = "" management_token = "" - rate_limit = 8.0 - rate_burst = 5 - max_retries = 3 + rate_limit = 8.0 + rate_burst = 5 + max_retries = 3 + retry_wait_min = 1 + retry_wait_max = 30 } resource "contentstack_content_type" "example" { diff --git a/go.mod b/go.mod index 2d7101d..273525b 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,12 @@ go 1.24.0 toolchain go1.24.3 require ( + github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/terraform-plugin-docs v0.9.0 github.com/hashicorp/terraform-plugin-framework v0.8.1-0.20220531184835-f0051665855e github.com/hashicorp/terraform-plugin-go v0.9.1 github.com/labd/contentstack-go-sdk v0.1.0 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.2 ) // replace github.com/labd/contentstack-go-sdk => ../contentstack-go-sdk @@ -21,14 +22,14 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.4 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -42,8 +43,8 @@ require ( github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/cli v1.1.4 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -59,12 +60,12 @@ require ( github.com/zclconf/go-cty v1.10.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.13.0 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20200711021454-869866162049 // indirect google.golang.org/grpc v1.46.0 // indirect google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2aa92f4..c17d681 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= @@ -105,13 +106,15 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ= github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -165,16 +168,16 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/cli v1.1.4 h1:qj8czE26AU4PbiaPXK5uVmMSM+V5BYsFBiM9HhGRLUA= github.com/mitchellh/cli v1.1.4/go.mod h1:vTLESy5mRhKOs9KDp0/RATawxP1UqBmdrpVRMnpcvKQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -223,8 +226,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -279,11 +283,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -297,8 +299,11 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -362,7 +367,8 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 758efae..54d3eee 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,13 +3,31 @@ package provider import ( "context" "net/http" + "time" + "github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/labd/contentstack-go-sdk/management" ) +// retryableRoundTripper implements http.RoundTripper using retryablehttp.Client +type retryableRoundTripper struct { + retryClient *retryablehttp.Client +} + +// RoundTrip implements the http.RoundTripper interface +func (r *retryableRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // Convert http.Request to retryablehttp.Request + retryReq, err := retryablehttp.FromRequest(req) + if err != nil { + return nil, err + } + // Use the retryable client's Do method + return r.retryClient.Do(retryReq) +} + func New(version string) func() tfsdk.Provider { return func() tfsdk.Provider { return &provider{version: version} @@ -66,7 +84,17 @@ func (p *provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) "max_retries": { Type: types.Int64Type, Optional: true, - Description: "The maximum number of retry attempts for 429 (rate limit) responses. Defaults to 3. Uses exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s.", + Description: "The maximum number of retry attempts for 429 (rate limit) responses. Defaults to 3. Uses exponential backoff with jitter.", + }, + "retry_wait_min": { + Type: types.Int64Type, + Optional: true, + Description: "The minimum wait time in seconds between retries. Defaults to 1 second.", + }, + "retry_wait_max": { + Type: types.Int64Type, + Optional: true, + Description: "The maximum wait time in seconds between retries. Defaults to 30 seconds.", }, }, }, nil @@ -82,10 +110,11 @@ type providerData struct { RateLimit types.Float64 `tfsdk:"rate_limit"` RateBurst types.Int64 `tfsdk:"rate_burst"` MaxRetries types.Int64 `tfsdk:"max_retries"` + RetryWaitMin types.Int64 `tfsdk:"retry_wait_min"` + RetryWaitMax types.Int64 `tfsdk:"retry_wait_max"` } func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) { - // Retrieve provider data from configuration var config providerData diags := req.Config.Get(ctx, &config) @@ -94,15 +123,53 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq return } + retryClient := retryablehttp.NewClient() + + maxRetries := int(config.MaxRetries.Value) + if maxRetries <= 0 { + maxRetries = 3 // Default to 3 retries + } + retryClient.RetryMax = maxRetries + + retryWaitMin := time.Duration(config.RetryWaitMin.Value) * time.Second + if retryWaitMin <= 0 { + retryWaitMin = 1 * time.Second // Default to 1 second + } + retryClient.RetryWaitMin = retryWaitMin + + retryWaitMax := time.Duration(config.RetryWaitMax.Value) * time.Second + if retryWaitMax <= 0 { + retryWaitMax = 30 * time.Second // Default to 30 seconds + } + retryClient.RetryWaitMax = retryWaitMax + + // Custom retry policy to only retry on 429 responses + retryClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { + // Always retry on request errors + if err != nil { + return retryablehttp.DefaultRetryPolicy(ctx, resp, err) + } + // Only retry on 429 (rate limit) responses + if resp.StatusCode == 429 { + return true, nil + } + return false, nil + } + + // Create a custom RoundTripper that uses the retryable client + roundTripper := &retryableRoundTripper{retryClient: retryClient} + + // Always use our retryable transport (ignore debug transport for now) + httpClient := &http.Client{ + Transport: roundTripper, + } + cfg := management.ClientConfig{ - BaseURL: config.BaseURL.Value, - AuthToken: config.AuthToken.Value, - HTTPClient: &http.Client{ - Transport: management.DebugTransport, - }, + BaseURL: config.BaseURL.Value, + AuthToken: config.AuthToken.Value, + HTTPClient: httpClient, RateLimit: config.RateLimit.Value, RateBurst: int(config.RateBurst.Value), - MaxRetries: int(config.MaxRetries.Value), } c, err := management.NewClient(cfg)