diff --git a/docs/index.md b/docs/index.md
index e697833..3c50a15 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -31,6 +31,7 @@ provider "hydra" {
### Optional
- `authentication` (Block List, Max: 1) Optional block to specify an authentication method which is used to access Hydra Admin API. (see [below for nested schema](#nestedblock--authentication))
+- `retry` (Block List, Max: 1) Optional block to configure retry behavior for API requests. (see [below for nested schema](#nestedblock--retry))
### Nested Schema for `authentication`
@@ -88,4 +89,16 @@ Required:
Optional:
-- `insecure_skip_verify` (Boolean) Controls whether a client verifies the server's certificate chain and host name.
\ No newline at end of file
+- `insecure_skip_verify` (Boolean) Controls whether a client verifies the server's certificate chain and host name.
+
+
+
+
+### Nested Schema for `retry`
+
+Optional:
+
+- `enabled` (Boolean) Enable or disable retry behavior.
+- `max_elapsed_time` (String) Maximum time to spend retrying requests.
+- `max_interval` (String) Maximum interval between retries.
+- `randomization_factor` (Number) Randomization factor to add jitter to retry intervals.
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9d92e94..e3ed5c5 100644
--- a/go.mod
+++ b/go.mod
@@ -43,6 +43,7 @@ require (
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0
github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
diff --git a/go.sum b/go.sum
index 3b1c7b3..5e6c520 100644
--- a/go.sum
+++ b/go.sum
@@ -26,6 +26,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
diff --git a/internal/provider/data_source_jwks.go b/internal/provider/data_source_jwks.go
index c6f01a2..fe6f4c0 100644
--- a/internal/provider/data_source_jwks.go
+++ b/internal/provider/data_source_jwks.go
@@ -2,6 +2,7 @@ package provider
import (
"context"
+ "net/http"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -31,8 +32,17 @@ A JSON Web Key is identified by its set and key id. ORY Hydra uses this function
func readJWKSDataSource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
data.SetId(data.Get("name").(string))
- hydraClient := meta.(*hydra.APIClient)
- jsonWebKeySet, _, err := hydraClient.JwkApi.GetJsonWebKeySet(ctx, data.Id()).Execute()
+ hydraClient := meta.(*HydraConfig).hydraClient
+
+ var jsonWebKeySet *hydra.JsonWebKeySet
+
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ var err error
+ var resp *http.Response
+ jsonWebKeySet, resp, err = hydraClient.JwkApi.GetJsonWebKeySet(ctx, data.Id()).Execute()
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
+
if err != nil {
return diag.FromErr(err)
}
diff --git a/internal/provider/helper.go b/internal/provider/helper.go
index 09eadac..9aaa4b0 100644
--- a/internal/provider/helper.go
+++ b/internal/provider/helper.go
@@ -1,8 +1,11 @@
package provider
import (
+ "fmt"
+ "net/http"
"time"
+ "github.com/cenkalti/backoff/v4"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
@@ -32,3 +35,41 @@ func diffSuppressMatchingDurationStrings(k, old, new string, d *schema.ResourceD
return oldDuration == newDuration
}
+
+// retryThrottledHydraAction executes the fn function and if backOff is set, retries the function if the request is throttled.
+func retryThrottledHydraAction(fn func() (*http.Response, error), backOff *backoff.ExponentialBackOff) error {
+ if backOff == nil {
+ _, err := fn()
+ return err
+ }
+
+ retryAction := func() error {
+ resp, err := fn()
+
+ if err != nil {
+ if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
+ fmt.Println("Throttled, retrying...")
+ return err
+ }
+
+ return backoff.Permanent(err)
+ }
+
+ return nil
+ }
+
+ return backoff.Retry(retryAction, backOff)
+}
+
+func validateDuration(val interface{}, key string) (ws []string, errors []error) {
+ v, ok := val.(string)
+ if !ok {
+ errors = append(errors, fmt.Errorf("expected type of %s to be string", key))
+ return
+ }
+
+ if _, err := time.ParseDuration(v); err != nil {
+ errors = append(errors, fmt.Errorf("%q must be a valid duration string: %s", key, err))
+ }
+ return
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 1b208ac..2ba3a09 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -7,7 +7,9 @@ import (
"net/http"
"net/url"
"strings"
+ "time"
+ "github.com/cenkalti/backoff/v4"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -20,6 +22,11 @@ func init() {
schema.DescriptionKind = schema.StringMarkdown
}
+type HydraConfig struct {
+ hydraClient *hydra.APIClient
+ backOff *backoff.ExponentialBackOff
+}
+
func New() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
@@ -28,6 +35,42 @@ func New() *schema.Provider {
Required: true,
DefaultFunc: schema.EnvDefaultFunc("HYDRA_ADMIN_URL", nil),
},
+ "retry": {
+ Type: schema.TypeList,
+ Optional: true,
+ MaxItems: 1,
+ Description: "Optional block to configure retry behavior for API requests.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "enabled": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Default: false,
+ Description: "Enable or disable retry behavior.",
+ },
+ "max_elapsed_time": {
+ Type: schema.TypeString,
+ Optional: true,
+ Default: "30s",
+ Description: "Maximum time to spend retrying requests.",
+ ValidateFunc: validateDuration,
+ },
+ "max_interval": {
+ Type: schema.TypeString,
+ Optional: true,
+ Default: "3s",
+ Description: "Maximum interval between retries.",
+ ValidateFunc: validateDuration,
+ },
+ "randomization_factor": {
+ Type: schema.TypeFloat,
+ Optional: true,
+ Default: 0.5,
+ Description: "Randomization factor to add jitter to retry intervals.",
+ },
+ },
+ },
+ },
"authentication": {
Type: schema.TypeList,
Optional: true,
@@ -188,7 +231,25 @@ func providerConfigure(ctx context.Context, data *schema.ResourceData) (interfac
},
}
- return hydra.NewAPIClient(cfg), nil
+ var backOff *backoff.ExponentialBackOff
+ if retry, ok := data.GetOk("retry.0"); ok && data.Get("retry.0.enabled").(bool) {
+ backOff = backoff.NewExponentialBackOff()
+
+ retryConfig := retry.(map[string]interface{})
+
+ maxElapsedTime, _ := time.ParseDuration(retryConfig["max_elapsed_time"].(string))
+ maxInterval, _ := time.ParseDuration(retryConfig["max_interval"].(string))
+ randomizationFactor := retryConfig["randomization_factor"].(float64)
+
+ backOff.MaxElapsedTime = maxElapsedTime
+ backOff.MaxInterval = maxInterval
+ backOff.RandomizationFactor = randomizationFactor
+ }
+
+ return &HydraConfig{
+ hydraClient: hydra.NewAPIClient(cfg),
+ backOff: backOff,
+ }, nil
}
func configureHTTPClient(data *schema.ResourceData) (*http.Client, error) {
diff --git a/internal/provider/resource_jwks.go b/internal/provider/resource_jwks.go
index 8b98356..9aee068 100644
--- a/internal/provider/resource_jwks.go
+++ b/internal/provider/resource_jwks.go
@@ -2,6 +2,7 @@ package provider
import (
"context"
+ "net/http"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -76,13 +77,16 @@ func createJWKSResource(ctx context.Context, data *schema.ResourceData, meta int
}
func generateJWKSResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
setName := data.Get("name").(string)
generators := data.Get("generator").([]interface{})
generator := generators[0].(map[string]interface{})
- _, _, err := hydraClient.JwkApi.CreateJsonWebKeySet(ctx, setName).CreateJsonWebKeySet(*dataToJWKGeneratorRequest(generator)).Execute()
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ _, resp, err := hydraClient.JwkApi.CreateJsonWebKeySet(ctx, setName).CreateJsonWebKeySet(*dataToJWKGeneratorRequest(generator)).Execute()
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
if err != nil {
return diag.FromErr(err)
}
@@ -93,8 +97,15 @@ func generateJWKSResource(ctx context.Context, data *schema.ResourceData, meta i
}
func readJWKSResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
- jsonWebKeySet, _, err := hydraClient.JwkApi.GetJsonWebKeySet(ctx, data.Id()).Execute()
+ hydraClient := meta.(*HydraConfig).hydraClient
+ var jsonWebKeySet *hydra.JsonWebKeySet
+
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ var err error
+ var resp *http.Response
+ jsonWebKeySet, resp, err = hydraClient.JwkApi.GetJsonWebKeySet(ctx, data.Id()).Execute()
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
if err != nil {
return diag.FromErr(err)
}
@@ -105,11 +116,14 @@ func readJWKSResource(ctx context.Context, data *schema.ResourceData, meta inter
}
func updateJWKSResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
setName := data.Get("name").(string)
- _, _, err := hydraClient.JwkApi.SetJsonWebKeySet(ctx, setName).JsonWebKeySet(*dataToJWKS(data, "key")).Execute()
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ _, resp, err := hydraClient.JwkApi.SetJsonWebKeySet(ctx, setName).JsonWebKeySet(*dataToJWKS(data, "key")).Execute()
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
if err != nil {
return diag.FromErr(err)
}
@@ -120,11 +134,13 @@ func updateJWKSResource(ctx context.Context, data *schema.ResourceData, meta int
}
func deleteJWKSResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
setName := data.Get("name").(string)
- _, err := hydraClient.JwkApi.DeleteJsonWebKeySet(ctx, setName).Execute()
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ return hydraClient.JwkApi.DeleteJsonWebKeySet(ctx, setName).Execute()
+ }, meta.(*HydraConfig).backOff)
if err != nil {
return diag.FromErr(err)
}
diff --git a/internal/provider/resource_oauth2_client.go b/internal/provider/resource_oauth2_client.go
index 69ce239..0973bd2 100644
--- a/internal/provider/resource_oauth2_client.go
+++ b/internal/provider/resource_oauth2_client.go
@@ -3,7 +3,7 @@ package provider
import (
"context"
"errors"
- "fmt"
+ "net/http"
"regexp"
"strings"
@@ -322,11 +322,18 @@ The default, if omitted, is for the UserInfo Response to return the Claims as a
}
func createOAuth2ClientResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
+
+ var oAuth2Client *hydra.OAuth2Client
client := dataToClient(data)
- oAuth2Client, _, err := hydraClient.OAuth2Api.CreateOAuth2Client(ctx).OAuth2Client(*client).Execute()
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ var err error
+ var resp *http.Response
+ oAuth2Client, resp, err = hydraClient.OAuth2Api.CreateOAuth2Client(ctx).OAuth2Client(*client).Execute()
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
if err != nil {
return diag.FromErr(err)
}
@@ -335,18 +342,27 @@ func createOAuth2ClientResource(ctx context.Context, data *schema.ResourceData,
}
func readOAuth2ClientResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
+
+ var oAuth2Client *hydra.OAuth2Client
+
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ var resp *http.Response
+ var err error
- oAuth2Client, _, err := hydraClient.OAuth2Api.GetOAuth2Client(ctx, data.Id()).Execute()
+ oAuth2Client, resp, err = hydraClient.OAuth2Api.GetOAuth2Client(ctx, data.Id()).Execute()
+
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
if err != nil {
var genericOpenAPIError *hydra.GenericOpenAPIError
- if errors.As(err, genericOpenAPIError) {
- if err, ok := genericOpenAPIError.Model().(hydra.ErrorOAuth2); ok && err.StatusCode != nil && *err.StatusCode == 401 {
+ if errors.As(err, &genericOpenAPIError) {
+ if apiError, ok := genericOpenAPIError.Model().(hydra.ErrorOAuth2); ok && apiError.StatusCode != nil && *apiError.StatusCode == 401 {
data.SetId("")
return nil
}
}
- fmt.Println("Error2 SVH: ", err)
+
return diag.FromErr(err)
}
@@ -354,11 +370,17 @@ func readOAuth2ClientResource(ctx context.Context, data *schema.ResourceData, me
}
func updateOAuth2ClientResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
oAuthClient := dataToClient(data)
- oAuthClient, _, err := hydraClient.OAuth2Api.SetOAuth2Client(ctx, data.Id()).OAuth2Client(*oAuthClient).Execute()
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ var err error
+ var resp *http.Response
+ oAuthClient, resp, err = hydraClient.OAuth2Api.SetOAuth2Client(ctx, data.Id()).OAuth2Client(*oAuthClient).Execute()
+
+ return resp, err
+ }, meta.(*HydraConfig).backOff)
if err != nil {
return diag.FromErr(err)
}
@@ -367,9 +389,11 @@ func updateOAuth2ClientResource(ctx context.Context, data *schema.ResourceData,
}
func deleteOAuth2ClientResource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
- hydraClient := meta.(*hydra.APIClient)
+ hydraClient := meta.(*HydraConfig).hydraClient
- _, err := hydraClient.OAuth2Api.DeleteOAuth2Client(ctx, data.Id()).Execute()
+ err := retryThrottledHydraAction(func() (*http.Response, error) {
+ return hydraClient.OAuth2Api.DeleteOAuth2Client(ctx, data.Id()).Execute()
+ }, meta.(*HydraConfig).backOff)
return diag.FromErr(err)
}