Skip to content

Commit

Permalink
Merge pull request #24 from cullenmcdermott/workaround-rate-limits
Browse files Browse the repository at this point in the history
Work around rate limit
  • Loading branch information
cullenmcdermott committed Feb 19, 2023
2 parents 6cf6e9f + 7043a01 commit 8ef106f
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.18
golang 1.19.4
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ description: |-

- `api_key` (String) API Key for Porkbun
- `base_url` (String) Override Porkbun Base URL
- `max_retries` (Number) Should only be changed if needing to work around Porkbun API rate limits
- `secret_key` (String) Secret Key for Porkbun
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
module github.com/cullenmcdermott/terraform-provider-porkbun

go 1.18
go 1.19

require (
github.com/hashicorp/terraform-plugin-docs v0.13.0
github.com/hashicorp/terraform-plugin-framework v0.11.1
github.com/hashicorp/terraform-plugin-go v0.14.0
github.com/hashicorp/terraform-plugin-log v0.7.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.21.0
github.com/nrdcg/porkbun v0.1.1
github.com/stretchr/testify v1.7.2
github.com/nrdcg/porkbun v0.2.0
github.com/stretchr/testify v1.8.1
golang.org/x/exp v0.0.0-20221230162634-c8adb6e14cba
)

require (
Expand Down Expand Up @@ -64,7 +65,7 @@ require (
github.com/zclconf/go-cty v1.10.0 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
Expand Down
17 changes: 12 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
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/nrdcg/porkbun v0.1.1 h1:gxVzQYfFUGXhnBax/aVugoE3OIBAdHgrJgyMPyY5Sjo=
github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk+CPoA=
github.com/nrdcg/porkbun v0.2.0 h1:ghaqPtIKcffba99epWFkK3VWf6TKJT9WMXMgaTqv95Y=
github.com/nrdcg/porkbun v0.2.0/go.mod h1:i0uLMn9ItFsLsSQIAeEu1wQ9/+6EvX1eQw15hulMMRw=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
Expand Down Expand Up @@ -230,14 +230,19 @@ github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand All @@ -264,6 +269,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221230162634-c8adb6e14cba h1:Qa0t0rPXHxjZBt3IJdkf6gku8KvMpBwiCmhAL5tYNEI=
golang.org/x/exp v0.0.0-20221230162634-c8adb6e14cba/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down Expand Up @@ -311,8 +318,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
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-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
Expand Down
32 changes: 29 additions & 3 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"os"
"strconv"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/provider"
Expand All @@ -20,13 +21,15 @@ type porkbunProvider struct {
client *porkbun.Client
configured bool
version string
MaxRetries int
}

// providerData can be used to store data from the Terraform configuration.
type providerData struct {
ApiKey types.String `tfsdk:"api_key"`
SecretKey types.String `tfsdk:"secret_key"`
BaseUrl types.String `tfsdk:"base_url"`
ApiKey types.String `tfsdk:"api_key"`
SecretKey types.String `tfsdk:"secret_key"`
BaseUrl types.String `tfsdk:"base_url"`
MaxRetries types.Int64 `tfsdk:"max_retries"`
}

func (p *porkbunProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
Expand Down Expand Up @@ -94,6 +97,23 @@ func (p *porkbunProvider) Configure(ctx context.Context, req provider.ConfigureR
c.BaseURL, _ = url.Parse(baseUrl)
}

if data.MaxRetries.Null {
if mr, ok := os.LookupEnv("PORKBUN_MAX_RETRIES"); ok {
mri, err := strconv.Atoi(mr)
if err != nil {
resp.Diagnostics.AddError(
"failed converting max retries",
err.Error(),
)
}
p.MaxRetries = mri
} else {
p.MaxRetries = 10
}
} else {
p.MaxRetries = int(data.MaxRetries.Value)
}

p.client = c
p.configured = true
}
Expand Down Expand Up @@ -129,6 +149,12 @@ func (p *porkbunProvider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Dia
Optional: true,
Type: types.StringType,
},
"max_retries": {
MarkdownDescription: "Should only be changed if needing to work around Porkbun API rate limits",
Required: false,
Optional: true,
Type: types.Int64Type,
},
},
}, nil
}
Expand Down
14 changes: 14 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package provider

import (
"net/url"

"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/nrdcg/porkbun"
)

func newPorkbunProvider(testUrl string) provider.Provider {
client := porkbun.New("sk1_foobarbaz", "pk1_foobarbaz")
client.BaseURL, _ = url.Parse(testUrl)
return &porkbunProvider{
client: client,
configured: true,
version: "test",
}
}

func protoV6ProviderFactories(url string) map[string]func() (tfprotov6.ProviderServer, error) {
return map[string]func() (tfprotov6.ProviderServer, error){
"porkbun": providerserver.NewProtocol6WithError(newPorkbunProvider(url)),
Expand Down
101 changes: 88 additions & 13 deletions internal/provider/resource_dns_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package provider
import (
"context"
"fmt"
"strconv"
"strings"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
Expand All @@ -14,13 +11,25 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/nrdcg/porkbun"
"golang.org/x/exp/slices"
"strconv"
"strings"
"time"
)

// Ensure provider defined types fully satisfy framework interfaces
var _ provider.ResourceType = porkbunDnsRecordResourceType{}
var _ resource.Resource = porkbunDnsRecordResource{}
var _ resource.ResourceWithImportState = porkbunDnsRecordResource{}

// The API returns a string of "SUCCESS" or "ERROR" except for when we're rate limited
// We get a 503 and the go library expects a string so we need to treat this as a string for now
var retryableCodes = []int{503}

var (
sleep = 10
)

type porkbunDnsRecordResourceType struct{}

func (t porkbunDnsRecordResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
Expand Down Expand Up @@ -102,6 +111,7 @@ type porkbunDnsRecordResource struct {

func (r porkbunDnsRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data porkbunDnsRecordResourceData
attempts := r.provider.MaxRetries

diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
Expand All @@ -119,7 +129,7 @@ func (r porkbunDnsRecordResource) Create(ctx context.Context, req resource.Creat
Notes: data.Notes.Value, // Not documented
}

id, err := r.provider.client.CreateRecord(ctx, data.Domain.Value, record)
id, err := retry(attempts, sleep, func() (int, error) { return r.provider.client.CreateRecord(ctx, data.Domain.Value, record) })
if err != nil {
resp.Diagnostics.AddError(
"Error creating DNS Record",
Expand All @@ -135,6 +145,7 @@ func (r porkbunDnsRecordResource) Create(ctx context.Context, req resource.Creat

func (r porkbunDnsRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data porkbunDnsRecordResourceData
attempts := r.provider.MaxRetries

diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
Expand All @@ -143,15 +154,20 @@ func (r porkbunDnsRecordResource) Read(ctx context.Context, req resource.ReadReq
return
}

records, err := r.provider.client.RetrieveRecords(ctx, data.Domain.Value)
getRecordsResult, err := retry(attempts, sleep, func() ([]porkbun.Record, error) { return r.getRecords(ctx, data.Domain.Value) })

if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not retrieve records for %s", data.Domain.Value),
fmt.Sprintf("Error: %s", err),
fmt.Sprintf(
`Could not retrieve records for %s.`,
data.Domain.Value,
),
fmt.Sprintf("Error: %s", err.Error()),
)
}
tflog.Info(ctx, fmt.Sprintf("Found records: %s", records))
for _, record := range records {

tflog.Info(ctx, fmt.Sprintf("Found records: %s", getRecordsResult))
for _, record := range getRecordsResult {
tflog.Info(ctx, fmt.Sprintf("This record is: %s", record.ID))
if record.ID == data.Id.Value {
data.Content.Value = record.Content
Expand All @@ -177,6 +193,7 @@ func (r porkbunDnsRecordResource) Read(ctx context.Context, req resource.ReadReq
func (r porkbunDnsRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data porkbunDnsRecordResourceData
var recordId string
attempts := r.provider.MaxRetries

diags := req.Plan.Get(ctx, &data)
req.State.GetAttribute(ctx, path.Root("id"), &recordId)
Expand All @@ -203,7 +220,7 @@ func (r porkbunDnsRecordResource) Update(ctx context.Context, req resource.Updat
)
}

err = r.provider.client.EditRecord(ctx, data.Domain.Value, intId, record)
err = retrySingleReturn(attempts, sleep, func() error { return r.provider.client.EditRecord(ctx, data.Domain.Value, intId, record) })
if err != nil {
resp.Diagnostics.AddError(
"Error updating the record",
Expand All @@ -222,8 +239,7 @@ func (r porkbunDnsRecordResource) Update(ctx context.Context, req resource.Updat

func (r porkbunDnsRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state porkbunDnsRecordResourceData

//var data exampleResourceData
attempts := r.provider.MaxRetries

diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
Expand All @@ -240,7 +256,7 @@ func (r porkbunDnsRecordResource) Delete(ctx context.Context, req resource.Delet
)
}

err = r.provider.client.DeleteRecord(ctx, state.Domain.Value, intId)
err = retrySingleReturn(attempts, sleep, func() error { return r.provider.client.DeleteRecord(ctx, state.Domain.Value, intId) })
if err != nil {
resp.Diagnostics.AddError(
"Error deleting record",
Expand All @@ -256,3 +272,62 @@ func (r porkbunDnsRecordResource) Delete(ctx context.Context, req resource.Delet
func (r porkbunDnsRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

// Originally from https://stackoverflow.com/questions/67069723/keep-retrying-a-function-in-golang
func retry[T any](attempts int, sleep int, f func() (T, error)) (result T, err error) {
for i := 0; i < attempts; i++ {
if i > 0 {
time.Sleep(time.Duration(sleep) * time.Second)
sleep *= 2
}
result, err = f()
if err == nil {
return result, nil
}
status, ok := err.(porkbun.Status)
if ok {
if status.Status != "SUCCESS" {
return result, fmt.Errorf("cannot be retried: %s", status.Error())
}
}
servererr, ok := err.(porkbun.ServerError)
if ok {
if !isRetryable(servererr.StatusCode) {
return result, fmt.Errorf("received error is not retryable: %s", servererr.Error())
}
}
}
return result, fmt.Errorf("after %d attempts, last error: %s", attempts, err)
}

func retrySingleReturn(attempts int, sleep int, f func() error) (err error) {
for i := 0; i < attempts; i++ {
if i > 0 {
time.Sleep(time.Duration(sleep) * time.Second)
sleep *= 2
}
err = f()
if err == nil {
return nil
}
err, ok := err.(porkbun.ServerError)
if ok {
if !isRetryable(err.StatusCode) {
return fmt.Errorf("received error is not retryable: %s", err)
}
}
}
return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
}

func (r porkbunDnsRecordResource) getRecords(ctx context.Context, domain string) ([]porkbun.Record, error) {
records, err := r.provider.client.RetrieveRecords(ctx, domain)
if err != nil {
return []porkbun.Record{}, err
}
return records, nil
}

func isRetryable(status int) bool {
return slices.Contains(retryableCodes, status)
}
Loading

0 comments on commit 8ef106f

Please sign in to comment.