Skip to content

Commit

Permalink
add support for retry configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
tjorri committed Apr 22, 2024
1 parent 45726fe commit 74a8b07
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 24 deletions.
15 changes: 14 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

<a id="nestedblock--authentication"></a>
### Nested Schema for `authentication`
Expand Down Expand Up @@ -88,4 +89,16 @@ Required:

Optional:

- `insecure_skip_verify` (Boolean) Controls whether a client verifies the server's certificate chain and host name.
- `insecure_skip_verify` (Boolean) Controls whether a client verifies the server's certificate chain and host name.



<a id="nestedblock--retry"></a>
### 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.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
14 changes: 12 additions & 2 deletions internal/provider/data_source_jwks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
41 changes: 41 additions & 0 deletions internal/provider/helper.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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 {

Check failure on line 40 in internal/provider/helper.go

View workflow job for this annotation

GitHub Actions / Test

`backOff` can be `github.com/cenkalti/backoff/v4.BackOff` (interfacer)
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
}
63 changes: 62 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 24 additions & 8 deletions internal/provider/resource_jwks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
48 changes: 36 additions & 12 deletions internal/provider/resource_oauth2_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package provider
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
"strings"

Expand Down Expand Up @@ -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)
}
Expand All @@ -335,30 +342,45 @@ 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)
}

return diag.FromErr(dataFromClient(data, oAuth2Client))
}

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)
}
Expand All @@ -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)
}
Expand Down

0 comments on commit 74a8b07

Please sign in to comment.