Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for retry configurations #20

Merged
merged 6 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_policy` (Block List, Max: 1) Optional block to configure retry behavior for API requests. (see [below for nested schema](#nestedblock--retry_policy))

<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_policy"></a>
### Nested Schema for `retry_policy`

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.(*ClientConfig).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.(*ClientConfig).backOff)

if err != nil {
return diag.FromErr(err)
}
Expand Down
42 changes: 42 additions & 0 deletions internal/provider/helper.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package provider

import (
"fmt"
"net/http"
"reflect"
"time"

"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

Expand Down Expand Up @@ -32,3 +36,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.BackOff) error {
if backOff == nil || reflect.ValueOf(backOff).IsNil() {
_, 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 ClientConfig 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_policy": {
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_policy.0"); ok && data.Get("retry_policy.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 &ClientConfig{
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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).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.(*ClientConfig).hydraClient

_, err := hydraClient.OAuth2Api.DeleteOAuth2Client(ctx, data.Id()).Execute()
err := retryThrottledHydraAction(func() (*http.Response, error) {
return hydraClient.OAuth2Api.DeleteOAuth2Client(ctx, data.Id()).Execute()
}, meta.(*ClientConfig).backOff)

return diag.FromErr(err)
}
Expand Down
Loading